mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
fix: 修复 inProcessRunner 权限响应后未 cleanup 的 interval 泄漏
权限请求得到响应后(批准/拒绝),pollInterval 和 abort listener 未被清理,导致 setInterval 永远运行。在长时间运行的 swarm 会话 中,每次权限请求都会泄漏一个 interval 和一个 listener。 修复:在成功/拒绝路径中调用 cleanup() 以清理 interval、 unregister callback 和移除 abort listener。添加 6 个测试 覆盖 permission callback 注册/处理/清理生命周期。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
- [x] #10 MCP HTTP/SSE 缓冲区累积 — 确认已实现 ✅
|
- [x] #10 MCP HTTP/SSE 缓冲区累积 — 确认已实现 ✅
|
||||||
- [x] #11 LRU 缓存键保留大 JSON — **已确认完整实现**:FileStateCache 使用 LRU 双重限制(max 100 条目 + maxSize 25MB)+ sizeCalculation,22 tests
|
- [x] #11 LRU 缓存键保留大 JSON — **已确认完整实现**:FileStateCache 使用 LRU 双重限制(max 100 条目 + maxSize 25MB)+ sizeCalculation,22 tests
|
||||||
- [x] #12 QueryEngine.mutableMessages 不收缩 — **已修复**:实现 snipCompactIfNeeded(按 removedUuids 过滤)+ snipProjection(边界检测 + 视图投影),28 tests
|
- [x] #12 QueryEngine.mutableMessages 不收缩 — **已修复**:实现 snipCompactIfNeeded(按 removedUuids 过滤)+ snipProjection(边界检测 + 视图投影),28 tests
|
||||||
|
- [x] #18 Permission Polling Interval 泄漏 — **已修复**:inProcessRunner 权限响应后未调用 cleanup(),导致 setInterval 永远运行 + abort listener 挂载,6 tests
|
||||||
|
|
||||||
## 总览
|
## 总览
|
||||||
---
|
---
|
||||||
@@ -581,12 +582,14 @@ if (snipResult !== undefined) {
|
|||||||
| #9 RC 权限 | `src/hooks/__tests__/replBridgePermissionHandlers.test.ts` | 8 |
|
| #9 RC 权限 | `src/hooks/__tests__/replBridgePermissionHandlers.test.ts` | 8 |
|
||||||
| #11 FileStateCache | `src/utils/__tests__/fileStateCache.test.ts` | 22 |
|
| #11 FileStateCache | `src/utils/__tests__/fileStateCache.test.ts` | 22 |
|
||||||
| #7 语言注册 | `packages/color-diff-napi/src/__tests__/language-registration.test.ts` | 7 |
|
| #7 语言注册 | `packages/color-diff-napi/src/__tests__/language-registration.test.ts` | 7 |
|
||||||
| **总计** | **6 个测试文件** | **72** |
|
| #18 Permission Polling | `src/hooks/__tests__/swarmPermissionPoller.test.ts` | 6 |
|
||||||
|
| **总计** | **7 个测试文件** | **78** |
|
||||||
```
|
```
|
||||||
|
|
||||||
### 需要关注的优先级
|
### 需要关注的优先级
|
||||||
|
|
||||||
1. ~~**P0 — `snipCompact.ts` 存根**~~ **已修复**
|
1. ~~**P0 — `snipCompact.ts` 存根**~~ **已修复**
|
||||||
2. **P1 — 语法按需加载回退**:highlight.js 190+ 语法常驻内存(~5-15MB),无释放时机
|
2. ~~**P1 — 语法按需加载回退**~~ **已修复**
|
||||||
3. ~~**P2 — NO_FLICKER 流状态**~~ **已修复**
|
3. ~~**P2 — NO_FLICKER 流状态**~~ **已修复**
|
||||||
4. **P2 — 空闲渲染循环**:框架存在但反编译代码中 keepAlive 集成完整性不确定
|
4. ~~**P2 — 空闲渲染循环**~~ **已确认完整**
|
||||||
|
5. ~~**P2 — Permission Polling Interval**~~ **已修复**
|
||||||
|
|||||||
107
src/hooks/__tests__/swarmPermissionPoller.test.ts
Normal file
107
src/hooks/__tests__/swarmPermissionPoller.test.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { afterEach, describe, expect, test } from 'bun:test'
|
||||||
|
import {
|
||||||
|
hasPermissionCallback,
|
||||||
|
processMailboxPermissionResponse,
|
||||||
|
registerPermissionCallback,
|
||||||
|
clearAllPendingCallbacks,
|
||||||
|
unregisterPermissionCallback,
|
||||||
|
} from '../../hooks/useSwarmPermissionPoller.js'
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearAllPendingCallbacks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('swarm permission poller registry', () => {
|
||||||
|
test('register and unregister callback', () => {
|
||||||
|
registerPermissionCallback({
|
||||||
|
requestId: 'req-1',
|
||||||
|
toolUseId: 'tool-1',
|
||||||
|
onAllow: () => {},
|
||||||
|
onReject: () => {},
|
||||||
|
})
|
||||||
|
expect(hasPermissionCallback('req-1')).toBe(true)
|
||||||
|
unregisterPermissionCallback('req-1')
|
||||||
|
expect(hasPermissionCallback('req-1')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('processMailboxPermissionResponse removes callback on approve', () => {
|
||||||
|
let approved = false
|
||||||
|
registerPermissionCallback({
|
||||||
|
requestId: 'req-2',
|
||||||
|
toolUseId: 'tool-2',
|
||||||
|
onAllow: () => { approved = true },
|
||||||
|
onReject: () => {},
|
||||||
|
})
|
||||||
|
const result = processMailboxPermissionResponse({
|
||||||
|
requestId: 'req-2',
|
||||||
|
decision: 'approved',
|
||||||
|
})
|
||||||
|
expect(result).toBe(true)
|
||||||
|
expect(approved).toBe(true)
|
||||||
|
// Callback is removed after processing
|
||||||
|
expect(hasPermissionCallback('req-2')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('processMailboxPermissionResponse removes callback on reject', () => {
|
||||||
|
let rejected = false
|
||||||
|
registerPermissionCallback({
|
||||||
|
requestId: 'req-3',
|
||||||
|
toolUseId: 'tool-3',
|
||||||
|
onAllow: () => {},
|
||||||
|
onReject: () => { rejected = true },
|
||||||
|
})
|
||||||
|
const result = processMailboxPermissionResponse({
|
||||||
|
requestId: 'req-3',
|
||||||
|
decision: 'rejected',
|
||||||
|
feedback: 'denied',
|
||||||
|
})
|
||||||
|
expect(result).toBe(true)
|
||||||
|
expect(rejected).toBe(true)
|
||||||
|
expect(hasPermissionCallback('req-3')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('processMailboxPermissionResponse returns false for unknown request', () => {
|
||||||
|
const result = processMailboxPermissionResponse({
|
||||||
|
requestId: 'unknown',
|
||||||
|
decision: 'approved',
|
||||||
|
})
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resetPermissionCallbacks clears all callbacks', () => {
|
||||||
|
registerPermissionCallback({
|
||||||
|
requestId: 'req-a',
|
||||||
|
toolUseId: 'tool-a',
|
||||||
|
onAllow: () => {},
|
||||||
|
onReject: () => {},
|
||||||
|
})
|
||||||
|
registerPermissionCallback({
|
||||||
|
requestId: 'req-b',
|
||||||
|
toolUseId: 'tool-b',
|
||||||
|
onAllow: () => {},
|
||||||
|
onReject: () => {},
|
||||||
|
})
|
||||||
|
clearAllPendingCallbacks()
|
||||||
|
expect(hasPermissionCallback('req-a')).toBe(false)
|
||||||
|
expect(hasPermissionCallback('req-b')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('callback is removed BEFORE invoking handler (prevents re-entrant leak)', () => {
|
||||||
|
const order: string[] = []
|
||||||
|
registerPermissionCallback({
|
||||||
|
requestId: 'req-order',
|
||||||
|
toolUseId: 'tool-order',
|
||||||
|
onAllow: () => {
|
||||||
|
// During callback execution, the callback should already be removed
|
||||||
|
order.push('callback')
|
||||||
|
order.push(`has:${hasPermissionCallback('req-order')}`)
|
||||||
|
},
|
||||||
|
onReject: () => {},
|
||||||
|
})
|
||||||
|
processMailboxPermissionResponse({
|
||||||
|
requestId: 'req-order',
|
||||||
|
decision: 'approved',
|
||||||
|
})
|
||||||
|
expect(order).toEqual(['callback', 'has:false'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -424,7 +424,8 @@ function createInProcessCanUseTool(
|
|||||||
feedback: parsed.error,
|
feedback: parsed.error,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return // Callback already resolves the promise
|
cleanup()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user