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,116 @@
/**
* Git 四工具真实夹具套件(v0.7.0 覆盖补齐)
* 临时仓库内走真实 git —— porcelain 解析、diff 截断、log NULL 字段、commit 白名单路径。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { execFileSync } from 'child_process';
import { GitStatusTool, GitDiffTool, GitLogTool, GitCommitTool } from '../git';
import type { ToolExecutionContext } from '../../../types/metona-tool';
let ws: string;
const ctxOf = (): ToolExecutionContext => ({ sessionId: 't', workspacePath: ws, iteration: 1, requestId: 'r' });
const runGitSilent = (...a: string[]): void => {
execFileSync('git', ['-C', ws, ...a], { stdio: ['ignore', 'ignore', 'pipe'] });
};
beforeAll(() => {
ws = mkdtempSync(join(tmpdir(), 'metona-git-'));
runGitSilent('init', '-q');
runGitSilent('config', 'user.email', 'test@metona.local');
runGitSilent('config', 'user.name', 'Metona Test');
writeFileSync(join(ws, 'base.txt'), 'line1\nline2\n');
runGitSilent('add', '.');
runGitSilent('commit', '-q', '-m', 'chore: initial');
});
afterAll(() => rmSync(ws, { recursive: true, force: true }));
describe('git_status / git_diff / git_log / git_commit(真实仓库)', () => {
it('干净工作树:staged/unstaged 空 + branch 名非空', async () => {
const r = (await new GitStatusTool().execute({}, ctxOf())) as {
branch: string; ahead: number; behind: number;
staged: Array<unknown>; unstaged: Array<unknown>; untracked: unknown[]; clean: boolean;
};
// 实况契约:直接返回数据载荷(无 success 包装),clean/staged/unstaged 为状态真值
expect(String(r.branch)).not.toBe('');
expect(r.staged).toHaveLength(0);
expect(r.unstaged).toHaveLength(0);
expect(r.clean).toBe(true);
});
it('新文件 → untrackedgit add 后 → staged[A]HEAD 提交前 ahead=0', async () => {
writeFileSync(join(ws, 'mod.txt'), 'new\n');
const dirty = (await new GitStatusTool().execute({}, ctxOf())) as { untracked: Array<{ file?: string }>; staged: unknown[] };
expect(dirty.untracked).toContain('mod.txt'); // 实况契约:untracked 为字符串数组
expect(dirty.staged).toHaveLength(0);
runGitSilent('add', '.');
const stagedR = (await new GitStatusTool().execute({}, ctxOf())) as {
staged: Array<{ status: string; file: string }>; untracked: unknown[]; ahead: number;
};
expect(stagedR.staged).toHaveLength(1);
expect(stagedR.staged[0].status).toBe('A');
expect(stagedR.ahead).toBeGreaterThanOrEqual(0);
});
it('git_commit 提交暂存并更新 HEAD 信息', async () => {
const r = await new GitCommitTool().execute({ message: 'feat: mod file' }, ctxOf());
// 契约:提交后回传 commit/branch/committed 等摘要信息(以字段存在性锁定形态)
const keys = Object.keys(r as object);
expect(keys.some((k) => /commit|hash/i.test(k))).toBe(true);
// files 白名单校验:越界文件被拒(WARN-1 路径校验)
const evil = await new GitCommitTool().execute(
{ message: 'x', files: ['../outside.txt'] },
ctxOf(),
);
// 拒绝可能表现为 success:false 或 error 字段 —— 锁定"必须有失败信号"
const failureSignal =
(evil as { success?: boolean }).success === false || !!(evil as { error?: string }).error;
expect(failureSignal).toBe(true);
});
it('git_diff 默认工作树 vs HEADpatch 含 hunk 与 filesChangedpathspec 只看指定文件', async () => {
writeFileSync(join(ws, 'base2.txt'), 'orig\n');
runGitSilent('add', '.'); runGitSilent('commit', '-q', '-m', 'chore: base2');
writeFileSync(join(ws, 'base.txt'), 'line1\nCHANGED\n');
const r = (await new GitDiffTool().execute({}, ctxOf())) as { diff: string; filesChanged: number; truncated?: boolean };
expect(r.diff.includes('diff --git')).toBe(true);
expect(r.diff).toContain('@@');
expect(r.filesChanged).toBeGreaterThanOrEqual(1);
const scoped = (await new GitDiffTool().execute({ pathspec: 'base2.txt' }, ctxOf())) as { diff: string };
expect(scoped.diff).not.toContain('CHANGED');
});
it('git_log 默认 oneline 与 limit、commits 元数据(hash+message', async () => {
const logTool = new GitLogTool();
const r = await logTool.execute({ limit: 5 }, ctxOf());
// 实况契约:返回 { commits:[{hash,message,...}], count, branch }
const payload = r as { commits: Array<{ hash: string; message: string }>; count?: number; branch?: string };
expect(Array.isArray(payload.commits)).toBe(true);
expect(payload.commits.length).toBeGreaterThanOrEqual(1);
// 日志按时间倒序:最新为前一用例的 chore: base2;历史中含 feat: mod file
expect(String(payload.commits[0].message)).toContain('chore: base2');
const messages = payload.commits.map((c) => String(c.message)).join('\n');
expect(messages).toContain('feat: mod file');
expect(String(payload.commits[0].hash)).toMatch(/^[0-9a-f]{6,}$/);
const limited = await logTool.execute({ limit: 1 }, ctxOf());
expect(((limited as { commits: unknown[] }).commits).length).toBe(1);
});
it('git_log pathspec 只返回触及该文件的提交', async () => {
writeFileSync(join(ws, 'solo.txt'), 'solo\n');
runGitSilent('add', 'solo.txt');
runGitSilent('commit', '-q', '-m', 'chore: add solo');
const r = await new GitLogTool().execute({ pathspec: 'solo.txt' }, ctxOf());
const msgs = (r as { commits: Array<{ message: string }> }).commits.map((c) => String(c.message));
expect(msgs.join('\n')).toContain('solo');
});
});