fix: v0.5.1 工具调用链路复检修复 — SubAgent 孤儿工具拦截 + 确认弹框会话隔离
背景:v0.5.0 发布后对工具调用链路(adapter 流式 tool_call → 引擎 PARSING → preToolHooks 管道 → ToolRegistry → 结果回填)做全链路复检,发现并修复两处问题。 安全修复: - SubAgent 中止时 pending 确认未清理(安全回归):SubEngine 以 taskId 为 sessionId 写入 ConfirmationHook,abortSession 的 clearPending(sessionId) 清不到它们。后果:中止会话后残留弹框若被补批,孤儿工具会真实执行副作用 (v0.4.x 的全局清空反而能拦截)。修复:abortByParent 返回被中止的 taskId 列表,abortSession 一并 clearPending(taskId)。 确认弹框会话隔离(前端对齐后端 v0.5.0 语义): - ConfirmationRequest 新增 sessionId 字段(主会话为 sessionId,SubAgent 为 taskId),弹框在会话 INIT/TERMINATED 时只清除该会话的请求 —— 修复并发 会话下任意会话结束误清其他会话等待中确认的问题 - 选中计数按当前 requests 收敛(selectedIds 残留 id 无害化) 测试(207 → 215 用例): - 新增引擎级工具调用链路集成测试 ×6(engine-toolchain.test.ts):真实 PermissionCheckHook + RateLimitHook + ConfirmationHook 管道 + 真实 ToolRegistry,覆盖 SAFE 工具直通 / HIGH 工具批准执行 / 拒绝以 Blocked 错误回传 LLM / 会话隔离端到端 / 工具异常不中断循环 / 策略拦截系统路径 - 新增 FTS 触发器 × NULL content 删除安全性测试 ×2:验证 truncateAfter (编辑重发/重新生成)删除 content=NULL 的 assistant 消息(模型仅发 tool_calls 的标准场景)不抛错且索引保持一致 - agent.test.ts 的 orchestrator mock 适配 abortByParent 新返回类型 验证: lint 0 problems / typecheck 双工程 0 errors / test:electron 215 全过 / build 成功
This commit is contained in:
@@ -166,6 +166,61 @@ describe.skipIf(!dbAvailable)('FTS5 会话内容搜索', () => {
|
||||
const msg = (db.prepare('SELECT COUNT(*) AS c FROM messages').get() as { c: number }).c;
|
||||
expect(fts).toBe(msg);
|
||||
});
|
||||
|
||||
it('删除 NULL content 消息不抛错且索引保持一致(truncateAfter 安全性)', () => {
|
||||
// 模拟真实会话混合场景:user 文本 + assistant(NULL, 仅 tool_calls) + tool 结果 + assistant 文本
|
||||
insertMessage('m7', 's3', 'user', 'ftsdelete-test 用户消息', 7000);
|
||||
insertMessage('m8', 's3', 'assistant', null, 7100);
|
||||
insertMessage('m9', 's3', 'tool', '工具结果包含 ftsdelete-test 关键词', 7200);
|
||||
insertMessage('m10', 's3', 'assistant', '最终回答', 7300);
|
||||
db.prepare(
|
||||
`INSERT INTO sessions (id, title, created_at, updated_at) VALUES ('s3', '会话三', 1, 1)`,
|
||||
).run();
|
||||
|
||||
const ftsBefore = (db.prepare('SELECT COUNT(*) AS c FROM messages_fts').get() as { c: number })
|
||||
.c;
|
||||
const msgBefore = (db.prepare('SELECT COUNT(*) AS c FROM messages').get() as { c: number }).c;
|
||||
expect(ftsBefore).toBe(msgBefore);
|
||||
|
||||
// 删除 NULL content 行 — delete 触发器以 old.content=NULL 执行 'delete' 命令
|
||||
expect(() => db.prepare(`DELETE FROM messages WHERE id = 'm8'`).run()).not.toThrow();
|
||||
|
||||
// 行数仍一致
|
||||
const ftsAfter = (db.prepare('SELECT COUNT(*) AS c FROM messages_fts').get() as { c: number })
|
||||
.c;
|
||||
const msgAfter = (db.prepare('SELECT COUNT(*) AS c FROM messages').get() as { c: number }).c;
|
||||
expect(ftsAfter).toBe(msgAfter);
|
||||
expect(ftsAfter).toBe(ftsBefore - 1);
|
||||
|
||||
// 索引仍可正常查询(未被 NULL 删除破坏)
|
||||
const hit = sessionService.searchMessages('ftsdelete-test');
|
||||
expect(hit.some((r: any) => r.sessionId === 's3')).toBe(true);
|
||||
});
|
||||
|
||||
it('混合 NULL/非 NULL 的批量删除(truncateAfter 全场景)不抛错', () => {
|
||||
// 再插入一批混合消息
|
||||
insertMessage('m11', 's3', 'assistant', null, 8000);
|
||||
insertMessage('m12', 's3', 'user', 'ftsdelete-test 第二轮', 8100);
|
||||
insertMessage('m13', 's3', 'assistant', null, 8200);
|
||||
|
||||
// 批量删除(模拟 truncateMessagesAfter 的 rowid 范围删除)
|
||||
expect(() =>
|
||||
db
|
||||
.prepare(
|
||||
`DELETE FROM messages WHERE session_id = 's3' AND rowid >= (SELECT rowid FROM messages WHERE id = 'm7')`,
|
||||
)
|
||||
.run(),
|
||||
).not.toThrow();
|
||||
|
||||
// s3 全删后行数一致,其他会话索引不受影响
|
||||
const fts = (db.prepare('SELECT COUNT(*) AS c FROM messages_fts').get() as { c: number }).c;
|
||||
const msg = (db.prepare('SELECT COUNT(*) AS c FROM messages').get() as { c: number }).c;
|
||||
expect(fts).toBe(msg);
|
||||
const s3Hit = sessionService.searchMessages('ftsdelete-test');
|
||||
expect(s3Hit.every((r: any) => r.sessionId !== 's3')).toBe(true);
|
||||
// 其他会话仍可搜索
|
||||
expect(sessionService.searchMessages('uniquekeyword').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!dbAvailable)('审计日志导出(v0.5.0)', () => {
|
||||
|
||||
Reference in New Issue
Block a user