feat: v0.7.0 四阶段全量迭代 — 修复面收口 · 安全纵深 · 架构还债 · 能力演进
CI / 类型检查 + Lint + 单元测试 (push) Failing after 5m45s
CI / 全量测试 (Electron ABI) (push) Failing after 5m22s
CI / 产物编译验证 (push) Successful in 10m3s

P1 修复面收口: v0.6.3 截断自愈推全量(Anthropic/Ollama/非流式/引擎兜底); SSE 上游错误帧检测进重试通道;
clearMessages 摘要游标根治; truncateResult 内联图片白名单统一; 前端四 bug(确认弹窗锁死/MemoryViewer/
Virtuoso Footer/abort 尾部过滤) + reasoning 缓冲跨迭代污染; 托盘通知过滤与新建会话死链接线

P2 安全纵深: MCP 审批闭环(ConfirmationHook×PolicyEngine 联动+重名拒注册); SSRF 收敛 ssrf-guard 共享模块
(web_fetch 双通道校验+重定向终态复检); Electron 加固(preload CJS 化→sandbox:true/CSP/权限白名单/will-navigate);
run_command cmd.exe 白名单通道元字符守门; diff_viewer 10MB 预检; Anthropic thinking 预算下限; Agnes 思考显式关闭

P3 架构还债: OpenAICompatibleAdapter 中间基类收敛四家样板; 错误分类单轨化(删 mapError/getFetchSignal,
超时显式 ETIMEDOUT); PRAGMA user_version 迁移版本化; 死代码清理专项(cn.ts/SHORTCUTS/ContextMenu 分支/
getWindowState/modifiedArgs/sandbox 空壳); i18next 引入; a11y 第一轮; SearXNG 页批量草稿模型统一

P4 能力演进: Ollama pull 可取消/capabilities 探测/num_ctx 实测缓存; UpdateService feed 比对式自动更新
(app:updateCheck IPC + StatusBar 入口); MiMo providerOptions(web_search 服务端工具/strict JSON);
web_fetch extract_mode=markdown(turndown); network.proxyUrl 全局代理(Chromium sessions+undici dispatcher)

测试: 264 → 507 用例(Electron ABI 全绿零跳过), 覆盖引擎压缩管线/重试竞速/MEMORY.md 闸门/file_editor 五操作/
filesystem 七工具实体夹具/git 真实仓库/SSE 错误帧/全线截断自愈/Provider 请求形态矩阵/SSRF 表测/钩子分级矩阵/
OutputValidator 全量/SLO 指标/MCP 安全纯函数/task_manager 链路/渲染层纯域/i18n 桥契约
This commit is contained in:
2026-08-27 17:06:58 +08:00
parent b6e2a8bd25
commit 3940716dc2
78 changed files with 6369 additions and 1341 deletions
@@ -0,0 +1,97 @@
/**
* UpdateService 测试(v0.6.4 P4-2
* 锁定:语义化版本比较表、feed 拉取/解析/状态映射、禁用态。
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
vi.mock('electron-log', () => ({
default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
}));
import { UpdateService, isNewerVersion } from '../update.service';
describe('isNewerVersion 语义化比较', () => {
it.each([
['0.7.0', '0.6.4', true],
['1.0.0', '0.9.9', true],
['0.6.10', '0.6.9', true],
['0.6.4', '0.6.4', false],
['0.6.3', '0.6.4', false],
['v0.7.0', '0.6.4', true], // 容忍 v 前缀
['not-a-version', '0.6.4', false], // 非法输入宁可不提示
['', '0.6.4', false],
// 预发布:同号正式版 > 预发布;候选预发布不提示升级
['0.6.5-beta', '0.6.4', true],
['0.6.4-beta', '0.6.4', false],
['0.6.4', '0.6.4-beta', true],
])('%s vs %s → %s', (candidate, current, expected) => {
expect(isNewerVersion(candidate, current)).toBe(expected);
});
});
describe('UpdateService.check 状态映射', () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('未配置 feed → disabled(不发起任何请求)', async () => {
const fetchSpy = vi.fn();
vi.stubGlobal('fetch', fetchSpy);
const svc = new UpdateService(() => '0.6.4', () => null);
const result = await svc.check();
expect(result.status).toBe('disabled');
expect(fetchSpy).not.toHaveBeenCalled();
});
it('feed 声明更新版本 → available 并透传下载直链与 notes', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ version: '0.7.0', url: 'https://dl.example/setup.exe', notes: '- fix x' }), { status: 200 }),
),
);
const svc = new UpdateService(() => '0.6.4', () => 'https://update.example/feed.json');
const result = await svc.check();
expect(result.status).toBe('available');
if (result.status === 'available') {
expect(result.latestVersion).toBe('0.7.0');
expect(result.downloadUrl).toBe('https://dl.example/setup.exe');
expect(result.notes).toBe('- fix x');
}
});
it('同版本 / 更旧 → up-to-date', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(new Response(JSON.stringify({ version: '0.6.3' }), { status: 200 })),
);
const svc = new UpdateService(() => '0.6.4', () => 'https://u.example/f.json');
const result = await svc.check();
expect(result.status).toBe('up-to-date');
if (result.status === 'up-to-date') expect(result.latestVersion).toBe('0.6.3');
});
it('HTTP 失败与非法 JSON → error', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('oops', { status: 500 })));
const svc = new UpdateService(() => '0.6.4', () => 'https://u.example/f.json');
expect((await svc.check()).status).toBe('error');
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('<html/>', { status: 200 })));
expect((await svc.check()).status).toBe('error');
});
it('下载 URL 非 http(s) 时被剔除(防协议劫持)', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ version: '9.9.9', url: 'file:///C:/evil.exe' }), { status: 200 }),
),
);
const svc = new UpdateService(() => '0.6.4', () => 'https://u.example/f.json');
const result = await svc.check();
expect(result.status).toBe('available');
if (result.status === 'available') expect(result.downloadUrl).toBeUndefined();
});
});