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
@@ -255,3 +255,304 @@ describe.skipIf(!dbAvailable)('敏感 key — 全局层密文落盘', () => {
expect(rawFile).toContain('metona-enc:v1:');
});
});
describe.skipIf(!dbAvailable)('isUnconfiguredGlobalKey — 边界值矩阵', () => {
it.each([
// [key, value, expected]
['llm.apiKey', '', true],
['llm.apiKey', null, true],
['llm.apiKey', undefined, true],
['llm.apiKey', 0, false], // 数字 0 非空串非默认值
['llm.model', '', true],
// 默认值 → 未配置
['agent.maxIterations', 20, true],
['agent.totalTimeoutMs', 600000, true],
['agent.enableThinking', true, true],
['llm.maxTokens', 63488, true],
['onboarding.completed', false, true],
['ui.theme', 'auto', true],
// 非默认值 → 已配置
['agent.maxIterations', 21, false],
['agent.totalTimeoutMs', 1, false],
['agent.enableThinking', false, false], // 用户主动关闭
['llm.maxTokens', 1000, false],
['onboarding.completed', true, false], // 用户已完成引导
['ui.theme', 'dark', false],
// 非全局 key 恒未配置判定(不被 SEED_DEFAULTS 覆盖)
['searxng.enabled', 'anything', false],
['tools.write_file.enabled', false, false],
])('isUnconfiguredGlobalKey(%s, %j) → %j', (key, value, expected) => {
expect(isUnconfiguredGlobalKey(key, value)).toBe(expected);
});
it('isGlobalKey 边界:完整前缀匹配而非子串(deepseek.contextWindow 是全局 key', () => {
expect(isGlobalKey('deepseek.contextWindow')).toBe(true);
expect(isGlobalKey('deepseek.apiKey')).toBe(true);
expect(isGlobalKey('memory.consolidationEnabled')).toBe(true);
expect(isGlobalKey('memory.custom')).toBe(true);
// 非全局前缀
expect(isGlobalKey('workspace.path')).toBe(false);
expect(isGlobalKey('searxng.apiKey')).toBe(false);
expect(isGlobalKey('mcp.autoReconnect')).toBe(false);
});
});
describe.skipIf(!dbAvailable)('ConfigService.setBatch — 批量写入', () => {
it('setBatch 多 key 一次写入 DB 与全局层(全局 key 双写)', () => {
configService.setBatch([
{ key: 'llm.model', value: 'mimo-v3' },
{ key: 'agent.maxIterations', value: 40 },
{ key: 'ui.theme', value: 'light' },
]);
expect(configService.get<string>('llm.model')).toBe('mimo-v3');
expect(configService.get<number>('agent.maxIterations')).toBe(40);
expect(configService.get<string>('ui.theme')).toBe('light');
// 全局层同步
expect(globalConfig.get<string>('llm.model')).toBe('mimo-v3');
expect(globalConfig.get<number>('agent.maxIterations')).toBe(40);
});
it('setBatch 敏感 key 加密落盘、读取解密', () => {
configService.setBatch([{ key: 'llm.apiKey', value: 'sk-batch-secret' }]);
const raw = db!.prepare("SELECT value FROM app_config WHERE key = 'llm.apiKey'").get() as {
value: string;
};
expect(raw.value).toContain('metona-enc:v1:');
expect(configService.get<string>('llm.apiKey')).toBe('sk-batch-secret');
expect(globalConfig.get<string>('llm.apiKey')).toBe('sk-batch-secret');
});
it('setBatch 保留已有 category(与 set 语义一致)', () => {
db!
.prepare(
"INSERT INTO app_config (key, value, category) VALUES ('ui.theme', '\"blue\"', 'ui')",
)
.run();
configService.setBatch([{ key: 'ui.theme', value: 'dark' }]);
const row = db!.prepare("SELECT category FROM app_config WHERE key = 'ui.theme'").get() as {
category: string;
};
expect(row.category).toBe('ui');
});
it('setBatch 非全局 key 不写全局层', () => {
configService.setBatch([{ key: 'workspace.localOnly', value: 'x' }]);
expect(globalConfig.get<string>('workspace.localOnly')).toBeNull();
});
it('setBatch 空数组不抛错且无副作用', () => {
expect(() => configService.setBatch([])).not.toThrow();
expect(globalConfig.getAll()).toEqual({});
});
it('setBatch 部分失败(JSON.stringify 抛错)→ DB 事务整体回滚,不产生部分写入', () => {
const circular: Record<string, unknown> = {};
circular.self = circular; // 无法 JSON 序列化
expect(() =>
configService.setBatch([
{ key: 'ui.theme', value: 'dark' },
{ key: 'agent.maxIterations', value: circular },
]),
).toThrow();
// 事务回滚:第一个 key 也不应残留
const row = db!.prepare("SELECT value FROM app_config WHERE key = 'ui.theme'").get();
expect(row).toBeUndefined();
// 全局层不被触碰(事务异常中断在 DB 阶段)
expect(globalConfig.get<string>('ui.theme')).toBeNull();
});
it('setBatch 更新 updated_at 时间戳', () => {
configService.setBatch([{ key: 'llm.model', value: 'a' }]);
const row = db!.prepare("SELECT updated_at FROM app_config WHERE key = 'llm.model'").get() as {
updated_at: number;
};
expect(row.updated_at).toBeGreaterThan(0);
});
});
describe.skipIf(!dbAvailable)('ConfigService — getByCategory / delete / 值类型', () => {
it('getByCategory 只返回该分类下的 key', () => {
// 直接按真实库的 category 灌入(set 默认 category=general,需先有分类)
configService.set('ui.theme', 'dark');
configService.set('llm.provider', 'deepseek');
configService.set('ui.fontSize', 14);
db!
.prepare("UPDATE app_config SET category = 'ui' WHERE key IN ('ui.theme', 'ui.fontSize')")
.run();
const ui = configService.getByCategory('ui');
expect(Object.keys(ui).sort()).toEqual(['ui.fontSize', 'ui.theme']);
expect(ui['ui.theme']).toBe('dark');
expect(ui['ui.fontSize']).toBe(14);
expect('llm.provider' in ui).toBe(false);
});
it('getByCategory 不存在的分类返回空对象', () => {
expect(configService.getByCategory('nonexistent')).toEqual({});
});
it('getByCategory 返回的敏感 key 值为密文(getByCategory 未做解密 —— 实际行为契约)', () => {
db!
.prepare("INSERT INTO app_config (key, value, category) VALUES ('llm.apiKey', '\"\"', 'llm')")
.run();
configService.set('llm.apiKey', 'sk-cat-secret');
const llm = configService.getByCategory('llm');
expect(llm['llm.apiKey']).toContain('metona-enc:v1:');
expect(llm['llm.apiKey']).not.toContain('sk-cat-secret');
// 对照:get() 仍解密
expect(configService.get<string>('llm.apiKey')).toBe('sk-cat-secret');
});
it('getByCategory 容忍损坏 JSON(原样字符串返回)', () => {
db!
.prepare("INSERT INTO app_config (key, value, category) VALUES ('ui.raw', '{bad-json', 'ui')")
.run();
const ui = configService.getByCategory('ui');
expect(ui['ui.raw']).toBe('{bad-json');
});
it('delete 非全局 key:删除后 get 返回 null(无全局回退)', () => {
configService.set('workspace.localFlag', 'x');
expect(configService.delete('workspace.localFlag')).toBe(true);
expect(configService.get('workspace.localFlag')).toBeNull();
expect(configService.delete('workspace.localFlag')).toBe(false);
expect(configService.delete('never-existed')).toBe(false);
});
it('delete 全局 keyDB 行删除但 get 经全局层回退仍可读(双写不联动删除)', () => {
configService.set('llm.model', 'mimo');
expect(configService.delete('llm.model')).toBe(true);
const row = db!.prepare("SELECT value FROM app_config WHERE key = 'llm.model'").get();
expect(row).toBeUndefined();
// 全局层保留 → get 回退命中
expect(globalConfig.get<string>('llm.model')).toBe('mimo');
expect(configService.get<string>('llm.model')).toBe('mimo');
});
it('set/get 支持对象与数组值(JSON 序列化语义)', () => {
configService.set('ui.pinnedSessions', ['a', 'b']);
configService.set('agent.metadata', { enabled: true, max: 3 });
expect(configService.get<Array<string>>('ui.pinnedSessions')).toEqual(['a', 'b']);
expect(configService.get<{ enabled: boolean; max: number }>('agent.metadata')).toEqual({
enabled: true,
max: 3,
});
});
it('set 布尔与 null 值可回读', () => {
configService.set('ui.flag', false);
configService.set('ollama.numCtx', null);
expect(configService.get<boolean>('ui.flag')).toBe(false);
expect(configService.get<null>('ollama.numCtx')).toBeNull();
});
it('历史明文敏感 key 平滑兼容:DB 存明文时 get 原样返回', () => {
// 模拟旧版本未加密落盘的 apiKey
db!
.prepare(
"INSERT INTO app_config (key, value, category) VALUES ('llm.apiKey', '\"sk-legacy-plain\"', 'llm')",
)
.run();
expect(configService.get<string>('llm.apiKey')).toBe('sk-legacy-plain');
});
});
describe.skipIf(!dbAvailable)('migrateFromWorkspaceDB — 边界矩阵', () => {
it('false 值且默认值非 false → 迁移(用户主动关闭)', () => {
const migrated = globalConfig.migrateFromWorkspaceDB({ 'agent.enableThinking': false });
expect(migrated).toBe(1);
expect(globalConfig.get<boolean>('agent.enableThinking')).toBe(false);
});
it('false 值且默认值即 false → 跳过(seedDefaults 灌入)', () => {
expect(globalConfig.migrateFromWorkspaceDB({ 'onboarding.completed': false })).toBe(0);
expect(globalConfig.migrateFromWorkspaceDB({ 'llm.multimodalEnabled': false })).toBe(0);
});
it('true 值且默认值 false → 迁移(用户主动开启)', () => {
expect(globalConfig.migrateFromWorkspaceDB({ 'llm.multimodalEnabled': true })).toBe(1);
expect(globalConfig.get<boolean>('llm.multimodalEnabled')).toBe(true);
});
it('true 值且默认值 true → 跳过', () => {
expect(globalConfig.migrateFromWorkspaceDB({ 'security.promptInjectionDefense': true })).toBe(
0,
);
expect(globalConfig.migrateFromWorkspaceDB({ 'agent.enableThinking': true })).toBe(0);
});
it('0 与空对象等非空非默认值 → 迁移', () => {
expect(globalConfig.migrateFromWorkspaceDB({ 'llm.temperature': 0 })).toBe(0); // 默认 0 → 跳过
expect(globalConfig.migrateFromWorkspaceDB({ 'llm.temperature': 0.5 })).toBe(1);
expect(globalConfig.migrateFromWorkspaceDB({ 'ui.customFlag': 'on' })).toBe(1);
});
it('敏感 key 迁移:全局 JSON 中为密文(无明文泄漏)', () => {
const migrated = globalConfig.migrateFromWorkspaceDB({ 'llm.apiKey': 'sk-mig-secret' });
expect(migrated).toBe(1);
expect(globalConfig.get<string>('llm.apiKey')).toBe('sk-mig-secret');
const rawFile = readFileSync(join(mockState.userDataDir, 'global-config.json'), 'utf-8');
expect(rawFile).not.toContain('sk-mig-secret');
expect(rawFile).toContain('metona-enc:v1:');
});
it('数值边界:默认值 float 精确相等判定', () => {
// deepseek.contextWindow 默认 1000000
expect(globalConfig.migrateFromWorkspaceDB({ 'deepseek.contextWindow': 1000000 })).toBe(0);
expect(globalConfig.migrateFromWorkspaceDB({ 'deepseek.contextWindow': 64000 })).toBe(1);
});
it('迁移后新 ConfigService 读取(跨空间共享生效)', () => {
const migrated = globalConfig.migrateFromWorkspaceDB({ 'llm.provider': 'ollama' });
expect(migrated).toBe(1);
const db2 = makeDb();
const freshConfig = new ConfigService(() => db2);
freshConfig.setGlobalConfig(globalConfig);
expect(freshConfig.get<string>('llm.provider')).toBe('ollama');
db2.close();
});
});
describe.skipIf(!dbAvailable)('配置分层 — 空值回退细节', () => {
it('DB 值等于 seed 默认值(0)时视为未配置 → 回退全局层真实值', () => {
// llm.temperature seed 默认 0 —— DB 写入 0 与默认值不可区分 → 回退全局层
db!
.prepare(
"INSERT INTO app_config (key, value, category) VALUES ('llm.temperature', '0', 'llm')",
)
.run();
globalConfig.set('llm.temperature', 1);
expect(configService.get<number>('llm.temperature')).toBe(1);
});
it('DB 值为非默认数字 0(ui.theme 默认是字符串)→ 视为已配置不回退', () => {
db!
.prepare("INSERT INTO app_config (key, value, category) VALUES ('ui.theme', '0', 'ui')")
.run();
globalConfig.set('ui.theme', 'dark');
expect(configService.get<number>('ui.theme')).toBe(0);
});
it('DB 有非空字符串但全局层为 null → 不覆盖为 null(返回 DB 值)', () => {
configService.set('llm.model', 'local-model');
// 全局层无该值
expect(configService.get<string>('llm.model')).toBe('local-model');
});
it('DB miss 且全局层为 null → get 返回 null', () => {
expect(configService.get<string>('llm.provider')).toBeNull();
});
it('getAll 合并时全局层未配置的 key 不进结果', () => {
const all = configService.getAll();
expect('llm.provider' in all).toBe(false);
});
it('getAll 中 DB 未配置但全局层已配置 → 全局值补齐', () => {
globalConfig.set('llm.provider', 'openai');
configService.set('ui.theme', 'dark');
const all = configService.getAll();
expect(all['llm.provider']).toBe('openai');
expect(all['ui.theme']).toBe('dark');
});
});