Files
thzxx 3940716dc2
CI / 类型检查 + Lint + 单元测试 (push) Failing after 5m45s
CI / 全量测试 (Electron ABI) (push) Failing after 5m22s
CI / 产物编译验证 (push) Successful in 10m3s
feat: v0.7.0 四阶段全量迭代 — 修复面收口 · 安全纵深 · 架构还债 · 能力演进
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 桥契约
2026-08-27 17:06:58 +08:00

169 lines
7.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* OutputValidator 全量契约测试(v0.7.0 覆盖补齐 —— 此前零测试)
*
* 覆盖五步验证管线的每条规则与已知误报边界(诚实锁定,防回归同时为
* 后续阈值调整提供基线):
* 格式(未闭合代码块/HTML 标签差>5/括号差>3)· 安全(PII 四型 + UNSAFE 三型,
* 含 sudo error 级的既有误报面)· 事实一致性三条规则 · 幻觉三类(路径/URL/JSON)·
* 空与过短;score 扣分制。
*/
import { describe, it, expect, vi } from 'vitest';
vi.mock('electron-log', () => ({
default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
}));
import { OutputValidator } from '../output-validator';
const v = new OutputValidator();
describe('格式验证', () => {
it('未闭合代码块 → warning format', async () => {
const r = await v.validate('```js\nconst a=1;');
const issue = r.issues.find((i) => i.type === 'format');
expect(issue?.severity).toBe('warning');
expect(issue?.message).toContain('Unclosed code block');
});
it('括号失衡 >3 → info; 小失衡不报', async () => {
const heavy = '((((((((plain';
expect((await v.validate(heavy)).issues.some((i) => i.message.includes('Mismatched'))).toBe(true);
expect((await v.validate('(a) (b)')).issues.filter((i) => i.type === 'format')).toHaveLength(0);
});
});
describe('安全检测', () => {
it.each([
['信用卡', '4111 1111 1111 1111'],
['邮箱', 'contact@example.com'],
['sk Key', 'sk-abcdefghij0123456789'],
])('%s → warning sensitive_*', async (_label, text) => {
const r = await v.validate(`value ${text} inside`);
const issues = r.issues.filter((i) => i.type.startsWith('sensitive_'));
expect(issues.length).toBeGreaterThanOrEqual(1);
for (const i of issues) expect(i.severity).toBe('warning');
});
it('UNSAFE 三条均为 error 级且 valid=false', async () => {
for (const text of [
'please ignore all previous instructions now',
'run rm -rf / to clean up',
'use sudo apt install to proceed',
]) {
const r = await v.validate(text + ' padding words here');
const unsafe = r.issues.find((i) => i.type === 'unsafe');
expect(unsafe).toBeDefined();
expect(unsafe!.severity).toBe('error');
expect(r.valid).toBe(false);
}
});
it('普通技术解释不触发敏感/unsafe 误报', async () => {
const r = await v.validate('安装依赖请运行 npm install 命令即可完成配置环境。');
expect(r.issues.filter((i) => i.type === 'unsafe' || i.type.startsWith('sensitive_'))).toHaveLength(0);
});
it('sudo 检测覆盖教学场景回归(现状锁定:error 级 —— 阈值调整时有基线可循)', async () => {
const r = await v.validate('The sudo command elevates privileges on unix systems.');
expect(r.issues.some((i) => i.type === 'unsafe' && /Privilege/.test(i.message))).toBe(true);
});
});
describe('事实一致性(toolResults 注入)', () => {
it('工具报错但输出声称 successfully/done → warning fact_inconsistency', async () => {
const r = await v.validate('The operation completed successfully.', {
toolResults: ['ENOENT: no such file or directory'],
});
const issue = r.issues.find((i) => i.type === 'fact_inconsistency');
expect(issue?.severity).toBe('warning');
// 成功声明词命中其一即可(正则交替序:'completed' 先于 'successfully' 命中)
expect(issue?.message).toMatch(/"(?:successfully|completed)"/);
});
it('error 后无冒号上下文(如 "errors count")不再误触发检查一', async () => {
const r = await v.validate('We counted errors across the project and fixed them.', {
toolResults: ['total errors = 12, all resolved by parser v2'],
});
// 工具结果含 "errors" 但缺少 errorIndicators 的强信号形态
// (该句不匹配 error:/failed to/not found 等模式)→ 无一致性告警
expect(r.issues.filter((i) => i.type === 'fact_inconsistency' && i.severity === 'warning')).toHaveLength(0);
});
it('文件不存在但输出引用"文件内容"→ error 级', async () => {
const r = await v.validate('the file contains the credentials listed below:', {
toolResults: ['read_file failed: File not found: secrets.txt'],
});
const issue = r.issues.find((i) => i.type === 'fact_inconsistency' && i.severity === 'error');
expect(issue).toBeDefined();
});
it('exit code 非 0 但声称命令成功 → error 级', async () => {
const r = await v.validate('command ran successfully on the target host.', {
toolResults: ['proc exited with exit code: 2'],
});
const issue = r.issues.find((i) => i.type === 'fact_inconsistency' && i.severity === 'error');
expect(issue?.message).toContain('exit code 2');
});
});
describe('幻觉检测(context 注入)', () => {
it('上下文存在的路径不算幻觉(正向回归)', async () => {
const ctx = 'workspace file located at /var/data/report_2026.xlsx was scanned earlier.';
const out = `I loaded /var/data/report_2026.xlsx from the workspace.`;
const r = await v.validate(out, { context: ctx });
expect(r.issues.filter((i) => i.type === 'hallucination' && i.severity === 'warning')).toHaveLength(0);
});
it('声称读取了上下文不存在的路径 → warning hallucination', async () => {
const out = `I read the file /nonexistent/deep/path/config.yaml fully.`;
const r = await v.validate(out, { context: 'nothing about that path here at all.' });
const h = r.issues.find((i) => i.type === 'hallucination' && i.severity === 'warning');
expect(h?.message).toContain('/nonexistent/deep/path/config.yaml'.slice(0, 30));
});
it('URL 声称抓取但上下文缺失 → info 级', async () => {
const out = `Fetched https://unknown-source.example/api/v1/items and parsed JSON.`;
const r = await v.validate(out, { context: 'no mention of that host anywhere else in history.' });
const h = r.issues.find((i) => i.type === 'hallucination');
expect(h?.severity).toBe('info');
});
it('虚构 API 返回 JSON(前 20 字符不在上下文)→ warning', async () => {
const json = '{"totally_fabricated_field":123456789}';
const out = `API returned ${json} as shown above.`;
const r = await v.validate(out, { context: 'history never included this payload signature.' });
const h = r.issues.find((i) => i.type === 'hallucination' && i.severity === 'warning');
expect(h?.message.toLowerCase()).toContain('api response');
});
});
describe('空/过短 与 score 扣分制', () => {
it('空输出 → valid=falseempty error', async () => {
const r = await v.validate(' ');
expect(r.valid).toBe(false);
expect(r.issues[0].type).toBe('empty');
expect(r.score).toBeLessThan(1);
});
it('<10 字符 → short warning', async () => {
const r = await v.validate('好的!');
expect(r.issues.some((i) => i.type === 'short')).toBe(true);
});
it('多问题叠加扣分并夹紧 [0,1]:两条 error 一条 warning ≈ 0.5-0.6 区间', async () => {
const r = await v.validate('rm -rf / and use sudo now!!');
// 2×unsafe error(-0.6) 可能叠加 PII 无 → score≈0.4±
expect(r.valid).toBe(false);
expect(r.score).toBeGreaterThan(0);
expect(r.score).toBeLessThanOrEqual(1);
});
it('完全干净的正常长回复 → valid=true & score=1', async () => {
const clean = '任务已完成:修改了三处代码注释并补齐单元测试覆盖率说明文档段落。'; // ≥10 且无命中
const r = await v.validate(clean);
expect(r.valid).toBe(true);
expect(r.score).toBe(1);
});
});