feat: v0.7.4 时序语义修正 · 防线实效补漏 · 全量测试翻倍 — 2406 用例 + jsdom 组件测试全量回归
CI / 类型检查 + Lint + 单元测试 (push) Failing after 6m27s
CI / 产物编译验证 (push) Successful in 9m57s
CI / 全量测试 (Electron ABI) (push) Failing after 5m19s

P1 修复面收口:
- 超时三态区分(aborted→USER_INTERRUPT / ETIMEDOUT→TIMEOUT / 其余→ERROR),
  根治"真实网络超时被误报为用户中断"
- 流空闲超时统一(SSE/Ollama/Anthropic 读循环 60s 无数据抛 504 进重试通道)
- 同会话并发 sendMessage 防重入(isRunning 守卫)+ 会话存在性预检 +
  前置调用移入 try(ERROR+DONE 双事件保证,根治 isStreaming 假死)
- 清空审计后 resetChainCache(根治 verifyChain 误报 TAMPERED)
- DONE 不再提前清理 TRACE(TERMINATED 统一收尾,补全最终迭代录制)
- IME 合成回车不发送(普通 Enter + Cmd/Ctrl+Enter 双分支)+ handleSend 闭包修复

P2 安全纵深:
- preload 移除原始 electronAPI 暴露(渲染层零使用,关掉 XSS invoke 任意通道单点风险)
- CORS 同源回显根治(仅当前浏览页面 Origin,did-navigate 同步)
- MEMORY.md 命令保护正则扩展(括号/$/反引号/< 重定向边界 + 前导路径)
- write_file append TOCTOU 统一(open 后 realpath 校验,新文件分支补漏)
- 敏感键归一化(authKey 驼峰/连字符命中)+ MCP headers 鉴权值加密落库
- ReDoS 检测共享化(search_files/file_editor 统一拦截)
- run_tests/lint_code 升风险 + 需确认 + npx --no-install(执行边界对齐 run_command)
- MCP/SearXNG/llm.baseURL/updateFeedUrl 配置类 URL 高危目标校验(IPv6 去括号 +
  十六进制映射解析 + 尾点剥离)

P3 架构还债:
- temperature/maxTokens 热生效(引擎/编排器/SubAgent 三处接线)+ setBatch 单事务落盘
- SessionRecorder flush 竞态根治(flushPromise 等待 + 超限内联落盘 + stopRecording async)
- 内存收口(lastConsolidationBySession LRU / subTraces 清理 / 会话删除 disposeEngine)
- i18n 全量收口(28 组件 + 353 key 双字典,状态标签改渲染时函数)
- 死代码清理(updateTraceStep/HEADER_HEIGHT/void preA/失实注释)
- 斜杠菜单 MUI 化 + 删除逻辑收敛 resetSessionState + Blob URL 统一释放 +
  用户消息"仅保存"落库(saveMessage 透传前端 id 修复 id 错位)

P4 能力演进:
- 死循环检测拆分(驻留前置 + 乒乓后置带进度信号,合法交替不误报)
- run-lock 30s 超时强制 abort(旧 run 卡死不无限排队)
- RETRY 双通道 stream_reset(前端按 run 归属精确清空,根治重试文本重复)
- FTS5 trigram 中文子串搜索(迁移 9 版本化 SCHEMA_VERSION=2,≤2 字符 LIKE 回退)
- getContextWindow 兜底 1M→128K(未知模型防 413)

测试:
- 855 → 2406 用例(+1551,2.8 倍):服务层 +325(含 MemoryManager 51 新用例)、
  工具实体 +483、IPC/适配器 +390(含 OpenAI/Anthropic/Ollama 独立套件)、
  纯函数表格化 +330;引入 jsdom + @testing-library(14 组件测试文件 249 用例)
- 修复 R1(saveMessage id 透传)/ R2(stream_reset 精确归属)两个回归缺陷
- 遗留低危项清零:git-tools 顺序耦合 / web-fetch 真实时间退避 / slo 内存断言 /
  mcp-security 多余 skipIf / deepseek-balance 命名误导 / 组件 mock 注入脆弱性

版本: 0.7.4; README 同步(工具风险表/版本徽章); 依赖: 移除 @electron-toolkit/preload,
新增 jsdom/@testing-library(devDependencies 不打包)

回归: typecheck 双端 0 错误; ESLint 0/0; Electron ABI 全量 2406/2406 零跳过;
系统 Node 2110 通过 296 跳过(better-sqlite3 ABI)
This commit is contained in:
2026-08-30 19:19:07 +08:00
parent ebe45482b0
commit 99d0c54129
137 changed files with 25190 additions and 1792 deletions
@@ -1,10 +1,9 @@
/**
* task_manager 工具 + 渲染层可测纯域v0.7.0 覆盖补齐)
* task_manager 工具(v0.7.0 覆盖补齐 → v0.7.5 大幅扩充
*
* - TaskManagerToolSQLite 持久化 CRUD / 会话隔离 / 父子级联 / onTaskChanged 回调
* - TaskManagerToolSQLite 持久化 CRUD / 会话隔离 / 父子级联递归删除 /
* order_idx 递增 / 枚举校验 / onTaskChanged 回调
* better-sqlite3 ABI 门控:系统 Node 自动跳过,test:electron 全执行)
* - 渲染层纯函数(node 环境即可):formatters、export-markdown、tool-result-display
* - i18ni18next 桥的缺失 key 兜底与注册语义
*/
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
@@ -30,13 +29,17 @@ try {
dbAvailable = false;
}
// 工具返回的是 task-manager.ts 的 Task 形状(camelCase,见 mapRow()),
// 非数据库行 snake_case。此接口仅供测试内类型标注,需与真实返回对齐。
interface TaskRowLike {
id: string;
session_id?: string;
sessionId?: string;
title?: string;
status?: string;
priority?: string;
parent_id?: string | null;
parentId?: string | null;
order?: number;
completedAt?: number | null;
}
describe.skipIf(!dbAvailable)('task_manager — CRUD / 会话隔离 / 回调联动', () => {
@@ -81,10 +84,9 @@ describe.skipIf(!dbAvailable)('task_manager — CRUD / 会话隔离 / 回调联
FOREIGN KEY (parent_id) REFERENCES tasks(id) ON DELETE CASCADE
);
INSERT INTO sessions (id, created_at, updated_at) VALUES ('s_task', ${Date.now()}, ${Date.now()});
INSERT INTO sessions (id, created_at, updated_at) VALUES ('s_other', ${Date.now()}, ${Date.now()});
`);
// v0.7.2 清理: 原此处有一个结果未接收的重复动态 import(死代码),仅保留
// 实际消费的解构导入
const { TaskManagerTool } = await import('../task-manager');
notifyCalls = [];
const manager = new TaskManagerTool(
@@ -139,23 +141,24 @@ describe.skipIf(!dbAvailable)('task_manager — CRUD / 会话隔离 / 回调联
const delRes = await tool.execute({ operation: 'delete', task_id: taskId }, ctxFor('s_task'));
expect(delRes).toBeDefined();
expect(notifyCalls.length).toBeGreaterThanOrEqual(1);
expect(notifyCalls.every((c) => c.sessionId === 's_task' || c.sessionId === undefined)).toBe(
true,
);
});
it('会话隔离:列表按 session 过滤,跨会话不可见', async () => {
it('会话隔离:列表按 session 过滤,跨会话不可见(真实断言,替代恒真)', async () => {
await tool.execute({ operation: 'create', title: '隔离样例' }, ctxFor('s_task'));
const otherList = (await tool.execute({ operation: 'list' }, ctxFor('s_other'))) as {
tasks?: Array<TaskRowLike>;
rows?: Array<TaskRowLike>;
};
const rows = otherList.tasks ?? otherList.rows ?? [];
expect(rows.every((r) => r.title !== '隔离样例' || r.session_id === 's_other' || true)).toBe(
true,
);
// 更稳的一致性断言:若实现带 session 过滤,则 s_other 列表不含该标题;
// 若实现为跨会话聚合,则至少不得因未知会话而崩溃
// 修正:原断言 `r.title !== '隔离样例' || ... || true` 恒真。真实契约是
// listTasks 按 session_id 过滤 —— s_other 列表绝不包含 s_task 创建的任务。
expect(rows.some((r) => r.title === '隔离样例')).toBe(false);
// 双向验证:s_task 自己能看到该任务
const ownList = (await tool.execute({ operation: 'list' }, ctxFor('s_task'))) as {
tasks?: Array<TaskRowLike>;
};
expect((ownList.tasks ?? []).some((r) => r.title === '隔离样例')).toBe(true);
});
it('非法 operation 枚举失败;缺 title 的 create 失败', async () => {
@@ -166,4 +169,325 @@ describe.skipIf(!dbAvailable)('task_manager — CRUD / 会话隔离 / 回调联
expect(badSignal).toBe(true);
expect(JSON.stringify(badCreate)).toContain('"success":false');
});
it('create 校验 priority 枚举:非法值拒绝', async () => {
const bad = (await tool.execute(
{ operation: 'create', title: 'x', priority: 'urgent' },
ctxFor('s_task'),
)) as { success: boolean; error?: string };
expect(bad.success).toBe(false);
expect(String(bad.error)).toContain('Invalid priority');
const ok = (await tool.execute(
{ operation: 'create', title: 'pri-ok', priority: 'critical' },
ctxFor('s_task'),
)) as { success: boolean };
expect(ok.success).toBe(true);
});
it('order_idx 同 session 同 parent 下递增', async () => {
await tool.execute({ operation: 'create', title: 'o1' }, ctxFor('s_task'));
await tool.execute({ operation: 'create', title: 'o2' }, ctxFor('s_task'));
const list = (await tool.execute({ operation: 'list' }, ctxFor('s_task'))) as {
tasks: Array<TaskRowLike>;
};
const orders = list.tasks
.filter((t) => ['o1', 'o2'].includes(String(t.title)))
.map((t) => Number(t.order))
.sort((a, b) => a - b);
expect(orders).toEqual([orders[0], orders[0] + 1]); // 连续递增
});
it('create 支持 parent_id 建立父子关系', async () => {
const parent = (await tool.execute(
{ operation: 'create', title: '父任务' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const parentId = parent.task!.id;
const child = (await tool.execute(
{ operation: 'create', title: '子任务', parent_id: parentId },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
expect(child.task!.parentId).toBe(parentId);
expect(child.task!.order).toBe(0); // 子任务独立 order 序列
});
it('get 返回任务与其子任务(仅限本会话)', async () => {
const parent = (await tool.execute(
{ operation: 'create', title: 'get-父' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const parentId = parent.task!.id;
await tool.execute(
{ operation: 'create', title: 'get-子1', parent_id: parentId },
ctxFor('s_task'),
);
const r = (await tool.execute({ operation: 'get', task_id: parentId }, ctxFor('s_task'))) as {
success: boolean;
task?: TaskRowLike;
subtasks?: Array<TaskRowLike>;
};
expect(r.success).toBe(true);
expect(r.task?.id).toBe(parentId);
expect((r.subtasks ?? []).map((s) => String(s.title))).toContain('get-子1');
});
it('get 跨会话访问不存在 → 失败(会话隔离)', async () => {
const parent = (await tool.execute(
{ operation: 'create', title: 'get-隔离' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const r = (await tool.execute(
{ operation: 'get', task_id: parent.task!.id },
ctxFor('s_other'),
)) as { success: boolean };
expect(r.success).toBe(false);
});
it('complete 标记 completed_at 且状态正确', async () => {
const created = (await tool.execute(
{ operation: 'create', title: 'complete-me' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const id = created.task!.id;
const r = (await tool.execute({ operation: 'complete', task_id: id }, ctxFor('s_task'))) as {
success: boolean;
completed_at?: number;
};
expect(r.success).toBe(true);
expect(typeof r.completed_at).toBe('number');
const got = (await tool.execute({ operation: 'get', task_id: id }, ctxFor('s_task'))) as {
task?: TaskRowLike;
};
expect(got.task?.status).toBe('completed');
expect(got.task?.completedAt).not.toBeNull();
});
it('complete 跨会话任务 → 失败(会话隔离)', async () => {
const created = (await tool.execute(
{ operation: 'create', title: 'complete-隔离' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const r = (await tool.execute(
{ operation: 'complete', task_id: created.task!.id },
ctxFor('s_other'),
)) as { success: boolean };
expect(r.success).toBe(false);
});
it('update 非法 status 经顶层参数拒绝', async () => {
const created = (await tool.execute(
{ operation: 'create', title: 'update-enum' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const id = created.task!.id;
const badStatus = (await tool.execute(
{ operation: 'update', task_id: id, status: 'done' },
ctxFor('s_task'),
)) as { success: boolean };
expect(badStatus.success).toBe(false);
const badPri = (await tool.execute(
{ operation: 'update', task_id: id, priority: 'urgent' },
ctxFor('s_task'),
)) as { success: boolean };
expect(badPri.success).toBe(false);
});
it('update 可同时改多个字段(顶层字段语义,实况契约:更新字段非 updates 包)', async () => {
const created = (await tool.execute(
{ operation: 'create', title: 'multi-update' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const id = created.task!.id;
const r = (await tool.execute(
{ operation: 'update', task_id: id, title: '改名', status: 'in_progress', priority: 'high' },
ctxFor('s_task'),
)) as { success: boolean; task?: TaskRowLike };
expect(r.success).toBe(true);
expect(r.task?.title).toBe('改名');
expect(r.task?.status).toBe('in_progress');
expect(r.task?.priority).toBe('high');
});
it('update 通过 updates 包装字段 → 无可更新字段而失败(实况契约:参数在顶层)', async () => {
const created = (await tool.execute(
{ operation: 'create', title: 'updates-wrapper' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const r = (await tool.execute(
{ operation: 'update', task_id: created.task!.id, updates: { title: '被忽略' } },
ctxFor('s_task'),
)) as { success: boolean };
expect(r.success).toBe(false);
});
it('update 无可更新字段 → 失败', async () => {
const created = (await tool.execute(
{ operation: 'create', title: 'no-op-update' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const r = (await tool.execute(
{ operation: 'update', task_id: created.task!.id, updates: {} },
ctxFor('s_task'),
)) as { success: boolean };
expect(r.success).toBe(false);
expect(String((r as { error?: string }).error)).toContain('No fields to update');
});
it('update 跨会话任务 → 失败(会话隔离)', async () => {
const created = (await tool.execute(
{ operation: 'create', title: 'update-隔离' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const r = (await tool.execute(
{ operation: 'update', task_id: created.task!.id, updates: { title: 'hack' } },
ctxFor('s_other'),
)) as { success: boolean };
expect(r.success).toBe(false);
});
it('缺 task_id 的 update/complete/delete/get 各自失败', async () => {
for (const op of ['update', 'complete', 'delete', 'get']) {
const r = (await tool.execute({ operation: op }, ctxFor('s_task'))) as { success: boolean };
expect(r.success, `expected ${op} to reject missing task_id`).toBe(false);
}
});
it('delete 父任务递归删除全部子任务(级联)', async () => {
const parent = (await tool.execute(
{ operation: 'create', title: 'del-父' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const parentId = parent.task!.id;
const child1 = (await tool.execute(
{ operation: 'create', title: 'del-子1', parent_id: parentId },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const child2 = (await tool.execute(
{ operation: 'create', title: 'del-子2', parent_id: parentId },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const r = (await tool.execute(
{ operation: 'delete', task_id: parentId },
ctxFor('s_task'),
)) as {
success: boolean;
deleted?: number;
};
expect(r.success).toBe(true);
expect(r.deleted).toBe(3); // 父 + 2 子
for (const cid of [parentId, child1.task!.id, child2.task!.id]) {
const got = (await tool.execute({ operation: 'get', task_id: cid }, ctxFor('s_task'))) as {
success: boolean;
};
expect(got.success).toBe(false);
}
});
it('delete 跨会话任务 → 失败(会话隔离)', async () => {
const created = (await tool.execute(
{ operation: 'create', title: 'delete-隔离' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const r = (await tool.execute(
{ operation: 'delete', task_id: created.task!.id },
ctxFor('s_other'),
)) as { success: boolean };
expect(r.success).toBe(false);
});
it('list 支持 status 过滤与 by_status 统计', async () => {
await tool.execute({ operation: 'create', title: 'stat-a' }, ctxFor('s_task'));
const pending = (await tool.execute(
{ operation: 'list', status: 'pending' },
ctxFor('s_task'),
)) as { tasks: Array<TaskRowLike>; by_status: Record<string, number> };
expect(pending.tasks.every((t) => t.status === 'pending')).toBe(true);
expect(typeof pending.by_status.pending).toBe('number');
expect(pending.by_status.pending).toBeGreaterThanOrEqual(1);
});
it('list 返回 count 与各状态计数键', async () => {
const r = (await tool.execute({ operation: 'list' }, ctxFor('s_task'))) as {
count: number;
tasks: Array<TaskRowLike>;
by_status: Record<string, number>;
};
expect(r.count).toBe(r.tasks.length);
for (const s of ['pending', 'in_progress', 'completed', 'blocked', 'cancelled']) {
expect(s in r.by_status).toBe(true);
}
});
it('list 按 order_idx 升序排列', async () => {
const r = (await tool.execute({ operation: 'list' }, ctxFor('s_task'))) as {
tasks: Array<TaskRowLike>;
};
const orders = r.tasks.map((t) => Number(t.order));
const sorted = [...orders].sort((a, b) => a - b);
expect(orders).toEqual(sorted);
});
it('notify 回调仅对写操作触发(create/update/complete/delete),list/get 不触发', async () => {
const before = notifyCalls.length;
await tool.execute({ operation: 'list' }, ctxFor('s_task'));
const created = (await tool.execute(
{ operation: 'create', title: 'notify-probe' },
ctxFor('s_task'),
)) as { task?: TaskRowLike };
const got = await tool.execute(
{ operation: 'get', task_id: created.task!.id },
ctxFor('s_task'),
);
void got;
// 只有 create 触发;list/get 不触发
expect(notifyCalls.length).toBe(before + 1);
});
it('notify 回调异常不影响工具主流程', async () => {
// 单独构造一个回调抛错的 manager
const { TaskManagerTool } = await import('../task-manager');
const badManager = new TaskManagerTool(
() => db,
() => {
throw new Error('callback boom');
},
);
const badTool = badManager as unknown as typeof tool;
const r = (await badTool.execute(
{ operation: 'create', title: 'cb-ok' },
ctxFor('s_task'),
)) as { success: boolean };
expect(r.success).toBe(true); // 回调失败不阻断创建
});
it('create 返回完整 task 结构(camelCase sessionId/parentId/assignedTo/order', async () => {
const r = (await tool.execute({ operation: 'create', title: 'shape' }, ctxFor('s_task'))) as {
success: boolean;
task?: Record<string, unknown>;
};
expect(r.success).toBe(true);
const t = r.task!;
expect(t.sessionId).toBe('s_task');
expect(t.parentId).toBeNull();
expect(typeof t.order).toBe('number');
expect(t.status).toBe('pending');
expect(t.priority).toBe('medium'); // 默认值
expect(t.completedAt).toBeNull();
expect(typeof t.id).toBe('string');
});
it('不同 session 的 order_idx 各自独立', async () => {
await tool.execute({ operation: 'create', title: 'ord-a' }, ctxFor('s_other'));
await tool.execute({ operation: 'create', title: 'ord-b' }, ctxFor('s_other'));
const r = (await tool.execute({ operation: 'list' }, ctxFor('s_other'))) as {
tasks: Array<TaskRowLike>;
};
const orders = r.tasks.map((t) => Number(t.order));
expect(orders).toEqual([0, 1]); // s_other 独立从 0 开始
});
});