/** * goal-contract.js 单测(R5-F3) * * 覆盖 27-P3/P4 §六 验收标准 1: * 「手写一份 goal-benchmark + goal-test-data + goal-test-script, * 执行器能对一个被测产物跑出明确的通过/不通过判决,判决落库可查」 * * 三段各跑一次「通过」和一次「不通过」;test_script 起真实 node 子进程, * 被测脚本就地生成(不依赖外部环境)。最后一组用例直接 spawn CLI, * 跑的就是三个 validator 模板 tools.md 里印的那几行命令——「实现在、零调用方」到此为止。 */ import { describe, it, expect, beforeEach, afterAll } from 'vitest'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { execFileSync } from 'child_process'; // R19-B:契约落点是 `{companyRoot}/tasks/{taskId}/`,公司根由代码推导(GRAPH_RUNS_DIR 可覆盖)。 // 隔离手法与 graph-engine.test.js 一致:**先设环境变量再 require**(两个模块都在加载时读一次), // 否则用例会往仓库真实的 data/companies/ 和 data/graph.sqlite 里写东西。 // vitest 4 的默认 pool 是 forks,process.env 是本进程私有的,不会串到别的用例文件。 const ENV_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'goal-contract-env-')); process.env.GRAPH_RUNS_DIR = ENV_ROOT; process.env.GRAPH_DB_PATH = path.join(ENV_ROOT, 'graph.sqlite'); const gc = require('../../services/goal-contract'); const wgdb = require('../../services/work-graph-db'); const CLI = path.resolve(__dirname, '../../services/goal-contract.js'); const ENGINE_SRC = path.resolve(__dirname, '../../services/graph-engine.js'); const tmpRoots = [ENV_ROOT]; /** 一个干净的 run 目录:data/companies/{cid}/runs/{rootRunId} 的等价物 */ function newRunDir() { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'goal-contract-')); tmpRoots.push(root); const runDir = path.join(root, 'runs', 'run_test'); fs.mkdirSync(path.join(runDir, 'artifacts'), { recursive: true }); return runDir; } function writeJson(abs, json) { fs.mkdirSync(path.dirname(abs), { recursive: true }); fs.writeFileSync(abs, JSON.stringify(json, null, 2)); return abs; } /** 被测产物:nodes/{nodeRunId}/attempt-{n}.json —— 就是 output_ref 指向的那个 */ function writeTarget(runDir, nodeRunId, attempt, json) { return writeJson(path.join(runDir, 'nodes', nodeRunId, `attempt-${attempt}.json`), json); } /** 就地生成一个最小 node 脚本当验收脚本;不依赖任何外部环境 */ function writeScript(runDir, rel, body) { const abs = path.join(runDir, 'artifacts', rel); fs.mkdirSync(path.dirname(abs), { recursive: true }); fs.writeFileSync(abs, body); return abs; } const BENCHMARK = { dimensions: [ { name: 'coverage', weight: 0.6, pass_threshold: 80 }, { name: 'readability', weight: 0.4, pass_threshold: 70 }, ], overall_pass_threshold: 78, }; const TEST_DATA = { match: 'deep-equal', cases: [ { id: 'c-normal', input: { n: 2 }, expect: { out: 4 } }, { id: 'c-edge', input: { n: 0 }, expect: { out: 0 } }, ], }; /** ContractValidationError 的 message 是固定的,细节在 .errors 里 */ function errorsOf(fn) { try { fn(); } catch (e) { return e.errors || [{ rule: '?', detail: e.message }]; } throw new Error('expected to throw'); } function runCli(args, opts = {}) { try { const stdout = execFileSync(process.execPath, [CLI, ...args], { encoding: 'utf8', ...opts }); return { code: 0, stdout, stderr: '' }; } catch (e) { return { code: e.status, stdout: String(e.stdout || ''), stderr: String(e.stderr || '') }; } } /** * R19-B 形态的 run 目录:`{GRAPH_RUNS_DIR}/{rootRunId}/artifacts/task-goal.json` * —— 契约落点是从这份身份卡推出来的(company.js 建 root run 时就写好了)。 */ let seq = 0; function newTaskRunDir({ companyId, taskId } = {}) { seq += 1; const cid = companyId || `co-${seq}`; const tid = taskId === undefined ? `task-${seq}` : taskId; const rootRunId = `gr_${seq}_${Date.now().toString(36)}`; const runDir = path.join(ENV_ROOT, rootRunId); writeJson(path.join(runDir, 'artifacts', 'task-goal.json'), { runId: rootRunId, companyId: cid, taskId: tid, task: '把契约落到 tasks/ 下', createdAt: new Date().toISOString(), }); return { runDir, companyId: cid, taskId: tid, rootRunId }; } afterAll(() => { for (const r of tmpRoots) fs.rmSync(r, { recursive: true, force: true }); delete process.env.GRAPH_RUNS_DIR; delete process.env.GRAPH_DB_PATH; }); // ──────────────────────────────────────────────────────────────── describe('契约 schema 校验', () => { it('合法契约通过(三段齐全,benchmark 走 $ref 外置)', () => { const runDir = newRunDir(); writeJson(path.join(runDir, 'artifacts', 'goal-benchmark.json'), BENCHMARK); writeScript(runDir, 'goal-test-script/acceptance.js', 'process.exit(0)\n'); const contract = { version: '1', goal_id: 'GOAL-ok', benchmark: { $ref: 'goal-benchmark.json' }, test_data: TEST_DATA, test_script: { runner: 'node', path: 'goal-test-script/acceptance.js' }, }; expect(gc.validateContract(contract)).toEqual({ ok: true, errors: [] }); const { contract_hash } = gc.writeContract(runDir, contract); expect(contract_hash).toMatch(/^sha256:/); // 读回时 $ref 已解开,且 hash 与写入时一致(内联写和 $ref 写必须同一个 hash) const loaded = gc.readContract(runDir); expect(loaded.contract.benchmark).toEqual(BENCHMARK); expect(gc.contractHash(loaded.contract)).toBe(contract_hash); }); it('非法契约被逐条挡下:空契约 / runner 越界 / 路径逃逸 / 权重不足 1 / 未知字段', () => { const rules = (c) => gc.validateContract(c).errors.map((e) => e.rule); // 空契约(三段全 null)——「平凡通过」的漏洞在格式层就堵掉 expect(rules({ version: '1', goal_id: 'G' })).toContain('section'); // runner 白名单 expect(rules({ version: '1', goal_id: 'G', test_script: { runner: 'bash', path: 'a.sh' } })).toContain('runner'); // 路径逃逸 expect(rules({ version: '1', goal_id: 'G', test_script: { runner: 'node', path: '../../../etc/passwd' }, })).toContain('reference'); // 权重之和 ≠ 1 expect(rules({ version: '1', goal_id: 'G', benchmark: { dimensions: [{ name: 'a', weight: 0.3 }] }, })).toContain('section'); // 版本 / 缺 goal_id / 未知字段 expect(rules({ version: '2', test_data: TEST_DATA, wat: 1 }).filter((r) => r === 'schema').length).toBe(3); // created_by 支持 {section: producer} 映射形态(三个产出节点各写一段) expect(gc.validateContract({ version: '1', goal_id: 'G', created_by: { benchmark: 'goal-benchmark' }, test_data: TEST_DATA, }).ok).toBe(true); expect(rules({ version: '1', goal_id: 'G', created_by: [1], test_data: TEST_DATA })).toContain('schema'); }); it('writeContract 拒绝非法契约,且 $ref 指向的文件不存在时就报错(不等到执行时才炸)', () => { const runDir = newRunDir(); expect(() => gc.writeContract(runDir, { version: '1', goal_id: 'G' })) .toThrow(gc.ContractValidationError); expect(() => gc.writeContract(runDir, { version: '1', goal_id: 'G', benchmark: { $ref: 'nope.json' }, })).toThrow(gc.ContractValidationError); expect(errorsOf(() => gc.writeContract(runDir, { version: '1', goal_id: 'G', benchmark: { $ref: 'nope.json' }, }))[0].detail).toMatch(/unreadable/); expect(fs.existsSync(gc.resolveContractPath(runDir))).toBe(false); }); }); // ──────────────────────────────────────────────────────────────── describe('三个产出节点各写一段,互不覆盖', () => { it('writeContractSection 合并写入,后写的不抹掉先写的', () => { const runDir = newRunDir(); writeJson(path.join(runDir, 'artifacts', 'goal-benchmark.json'), BENCHMARK); writeScript(runDir, 'goal-test-script/acceptance.js', 'process.exit(0)\n'); gc.writeContractSection(runDir, { section: 'benchmark', content: { $ref: 'goal-benchmark.json' }, goalId: 'GOAL-merge', createdBy: 'goal-benchmark', }); gc.writeContractSection(runDir, { section: 'test_data', content: TEST_DATA, goalId: 'GOAL-merge', createdBy: 'goal-test-data', }); const last = gc.writeContractSection(runDir, { section: 'test_script', content: { runner: 'node', path: 'goal-test-script/acceptance.js' }, goalId: 'GOAL-merge', createdBy: 'goal-test-script', }); expect(last.sections).toEqual(['benchmark', 'test_data', 'test_script']); const raw = JSON.parse(fs.readFileSync(gc.resolveContractPath(runDir), 'utf8')); expect(raw.benchmark).toEqual({ $ref: 'goal-benchmark.json' }); // 合并没有把 $ref 抄开 expect(raw.test_data).toEqual(TEST_DATA); expect(raw.created_by).toEqual({ benchmark: 'goal-benchmark', test_data: 'goal-test-data', test_script: 'goal-test-script', }); }); it('goal_id 串台直接拒绝(拿错 runDir 时不静默覆盖别人的契约)', () => { const runDir = newRunDir(); gc.writeContractSection(runDir, { section: 'test_data', content: TEST_DATA, goalId: 'GOAL-a' }); expect(errorsOf(() => gc.writeContractSection(runDir, { section: 'benchmark', content: BENCHMARK, goalId: 'GOAL-b', }))[0].detail).toMatch(/goal_id mismatch/); expect(errorsOf(() => gc.writeContractSection(runDir, { section: 'nope', content: {} }))[0].detail) .toMatch(/section must be one of/); // 被拒之后原契约一字未动 expect(gc.readContract(runDir).contract.goal_id).toBe('GOAL-a'); expect(gc.readContract(runDir).contract.benchmark).toBeUndefined(); }); }); // ──────────────────────────────────────────────────────────────── describe('执行器:benchmark', () => { const contract = { version: '1', goal_id: 'GOAL-bm', benchmark: BENCHMARK }; it('通过:逐维度过线且加权总分达标', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_a', 1, { scores: { coverage: 92, readability: 85 } }); const v = await gc.executeContract(contract, target, { runDir }); expect(v.ok).toBe(true); expect(v.fail_reason).toBe(null); expect(v.sections.benchmark.score).toBeCloseTo(89.2, 4); expect(v.failures).toEqual([]); expect(v.target).toBe(path.join('nodes', 'rn_a', 'attempt-1.json')); // run 目录内 → 相对路径 }); it('不通过:一维没过线 + 一维缺分', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_a', 1, { scores: { coverage: 61 } }); const v = await gc.executeContract(contract, target, { runDir }); expect(v.ok).toBe(false); expect(v.fail_reason).toBe('acceptance_not_met'); expect(v.failures.map((f) => f.case)).toEqual(['coverage', 'readability']); expect(v.failures[1].hint).toMatch(/missing score/); }); it('被测产物根本不存在 → 不通过(不是崩溃,也不是平凡通过)', async () => { const runDir = newRunDir(); const v = await gc.executeContract(contract, path.join(runDir, 'nodes/rn_x/attempt-1.json'), { runDir }); expect(v.ok).toBe(false); expect(v.sections.benchmark.reason).toBe('target_not_json'); }); }); // 35 号文档提案 D:evidence_hint 点名"浏览器/视口/开发者工具"类检查的维度, // 没有 evidence. 就不能凭一个分数通过——真实撞见过 validator 给这类维度 // 打满分却没做过对应检查(见 34 号文档)。 describe('执行器:benchmark 的证据闸门(35 号文档提案 D)', () => { const visualBenchmark = { dimensions: [ { name: 'hero_first_impression', weight: 0.5, pass_threshold: 80, evidence_hint: '打开浏览器截图首屏,量标题与标语的计算字号比例', }, { name: 'code_quality', weight: 0.5, pass_threshold: 70 }, // 不要求证据的普通维度 ], overall_pass_threshold: 75, }; const contract = { version: '1', goal_id: 'GOAL-visual', benchmark: visualBenchmark }; it('要求证据的维度只给分数、没给 evidence:即使分数达标也判不过', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_a', 1, { scores: { hero_first_impression: 100, code_quality: 90 }, }); const v = await gc.executeContract(contract, target, { runDir }); expect(v.ok).toBe(false); const f = v.failures.find((x) => x.case === 'hero_first_impression'); expect(f).toBeTruthy(); expect(f.hint).toMatch(/evidence_hint 要求真实检查/); }); it('给了 evidence. 之后,达标分数正常通过', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_a', 1, { scores: { hero_first_impression: 100, code_quality: 90 }, evidence: { hero_first_impression: 'Playwright 实测:标题48px/标语16px,比例3:1' }, }); const v = await gc.executeContract(contract, target, { runDir }); expect(v.ok).toBe(true); expect(v.failures).toEqual([]); }); it('不含"浏览器/视口/开发者工具"类关键词的普通维度,不受这道闸门约束(不用给 evidence)', async () => { const runDir = newRunDir(); // 只给 code_quality 一个维度的分数,hero_first_impression 缺分(走既有的"缺分"分支, // 不是这道新闸门),确认新闸门没有误伤跟证据无关的维度 const target = writeTarget(runDir, 'rn_a', 1, { scores: { code_quality: 90 } }); const v = await gc.executeContract(contract, target, { runDir }); const codeQuality = v.sections.benchmark.dimensions.find((d) => d.name === 'code_quality'); expect(codeQuality.ok).toBe(true); expect(codeQuality.evidence_required).toBeUndefined(); }); it('evidence 是空字符串 / 非字符串时按"没给"处理,不当真', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_a', 1, { scores: { hero_first_impression: 100, code_quality: 90 }, evidence: { hero_first_impression: ' ' }, }); const v = await gc.executeContract(contract, target, { runDir }); expect(v.ok).toBe(false); expect(v.failures.some((f) => f.case === 'hero_first_impression')).toBe(true); }); it('证据也认姊妹判词文件 attempt-.json 的 dimensions[].evidence——benchmark-validator/agent.md 早就在教这个写法', async () => { const runDir = newRunDir(); // target 是 attempt-1.scores.json(只有分数,Step 3 的写法) const target = writeTarget(runDir, 'rn_a', '1.scores', { scores: { hero_first_impression: 100, code_quality: 90 }, }); // 同目录的姊妹文件 attempt-1.json(判词文件,Step 5 的写法,带逐维度 evidence) writeTarget(runDir, 'rn_a', 1, { schema: 'work-validator/v1', validator: 'benchmark-validator', verdict: 'passed', dimensions: [ { name: 'hero_first_impression', score: 100, ok: true, evidence: 'Playwright 实测标题48px/标语16px,比例3:1' }, { name: 'code_quality', score: 90, ok: true, evidence: 'eslint 零告警' }, ], }); const v = await gc.executeContract(contract, target, { runDir }); expect(v.ok).toBe(true); expect(v.failures).toEqual([]); }); it('姊妹判词文件不存在 / 不是合法 JSON 时不拖垮打分,按"没有证据"正常走失败分支', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_a', '1.scores', { scores: { hero_first_impression: 100, code_quality: 90 }, }); // 故意不写 attempt-1.json(姊妹判词文件缺失) const v = await gc.executeContract(contract, target, { runDir }); expect(v.ok).toBe(false); expect(v.failures.some((f) => f.case === 'hero_first_impression')).toBe(true); }); }); // ──────────────────────────────────────────────────────────────── describe('执行器:test_data', () => { it('通过(subset 模式忽略额外键)', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_b', 1, { results: { 'c-normal': { out: 4, extra: 'ignored' }, 'c-edge': { out: 0 } }, }); const v = await gc.executeContract( { version: '1', goal_id: 'G', test_data: { ...TEST_DATA, match: 'subset' } }, target, { runDir }, ); expect(v.ok).toBe(true); expect(v.sections.test_data).toMatchObject({ total: 2, passed: 2, failed: 0, match: 'subset' }); }); it('不通过:一个值错、一个 case 缺结果(两者 hint 要分得开)', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_b', 1, { results: [{ id: 'c-normal', output: { out: 5 } }] }); const v = await gc.executeContract({ version: '1', goal_id: 'G', test_data: TEST_DATA }, target, { runDir }); expect(v.ok).toBe(false); expect(v.fail_reason).toBe('acceptance_not_met'); expect(v.sections.test_data.failed).toBe(2); expect(v.sections.test_data.failures[0].hint).toMatch(/mismatch/); expect(v.sections.test_data.failures[1].hint).toMatch(/no result for case c-edge/); }); }); // ──────────────────────────────────────────────────────────────── describe('执行器:test_script(真子进程)', () => { it('通过:脚本读到 ACCEPTANCE_TARGET 并 exit 0', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_c', 1, { answer: 42 }); writeScript(runDir, 'goal-test-script/acceptance.js', ` const fs = require('fs'); const t = JSON.parse(fs.readFileSync(process.env.ACCEPTANCE_TARGET, 'utf8')); process.exit(t.answer === 42 ? 0 : 1); `); const v = await gc.executeContract({ version: '1', goal_id: 'G', test_script: { runner: 'node', path: 'goal-test-script/acceptance.js', timeout_ms: 20000 }, }, target, { runDir }); expect(v.ok).toBe(true); expect(v.sections.test_script.exit_code).toBe(0); }); it('不通过:断言不成立 → exit 1,stderr 进 hint', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_c', 1, { answer: 41 }); writeScript(runDir, 'goal-test-script/acceptance.js', ` const fs = require('fs'); const t = JSON.parse(fs.readFileSync(process.env.ACCEPTANCE_TARGET, 'utf8')); if (t.answer !== 42) { console.error('answer=' + t.answer + ' expected 42'); process.exit(1); } `); const v = await gc.executeContract({ version: '1', goal_id: 'G', test_script: { runner: 'node', path: 'goal-test-script/acceptance.js', timeout_ms: 20000 }, }, target, { runDir }); expect(v.ok).toBe(false); expect(v.fail_reason).toBe('acceptance_not_met'); expect(v.sections.test_script.exit_code).toBe(1); expect(v.failures[0].hint).toMatch(/answer=41/); }); it('超时 → infra_flake(引擎侧原地重跑、不烧计数的那一档)', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_c', 1, {}); writeScript(runDir, 'goal-test-script/hang.js', 'setTimeout(() => {}, 30000);\n'); const v = await gc.executeContract({ version: '1', goal_id: 'G', test_script: { runner: 'node', path: 'goal-test-script/hang.js', timeout_ms: 300 }, }, target, { runDir }); expect(v.ok).toBe(false); expect(v.sections.test_script.timed_out).toBe(true); expect(v.fail_reason).toBe('infra_flake'); }, 20000); it('脚本文件缺失 / 路径逃逸都不起进程', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_c', 1, {}); const v = await gc.executeContract({ version: '1', goal_id: 'G', test_script: { runner: 'node', path: 'goal-test-script/nope.js' }, }, target, { runDir }); expect(v.sections.test_script.reason).toBe('script_missing'); }); it('契约本身不合法 → input_underspecified(不是产物的错)', async () => { const runDir = newRunDir(); const target = writeTarget(runDir, 'rn_c', 1, {}); const v = await gc.executeContract({ version: '1', goal_id: 'G' }, target, { runDir }); expect(v.ok).toBe(false); expect(v.fail_reason).toBe('input_underspecified'); }); }); // ──────────────────────────────────────────────────────────────── describe('判决持久化:落库后能读回', () => { let runDir; beforeEach(() => { runDir = newRunDir(); writeJson(path.join(runDir, 'artifacts', 'goal-benchmark.json'), BENCHMARK); gc.writeContract(runDir, { version: '1', goal_id: 'GOAL-persist', benchmark: { $ref: 'goal-benchmark.json' }, }); }); it('runAcceptance 读契约 → 执行 → 落盘,判决与索引都可读回', async () => { const good = writeTarget(runDir, 'rn_ok', 1, { scores: { coverage: 92, readability: 85 } }); const bad = writeTarget(runDir, 'rn_bad', 1, { scores: { coverage: 10, readability: 10 } }); const a = await gc.runAcceptance({ runDir, targetPath: good, runId: 'run_1', nodeRunId: 'rn_ok', attempt: 0 }); const b = await gc.runAcceptance({ runDir, targetPath: bad, runId: 'run_1', nodeRunId: 'rn_bad', attempt: 2 }); expect(a.verdict.ok).toBe(true); expect(b.verdict.ok).toBe(false); expect(fs.existsSync(a.path)).toBe(true); // 完整判决读得回,且四元组齐(attempt=0 是**第一次尝试**,不能被吞成 null) const back = gc.readVerdict(runDir, a.verdict.verdict_id); expect(back.contract_hash).toBe(a.verdict.contract_hash); expect(back.attempt).toBe(0); expect(back.run_id).toBe('run_1'); expect(back.node_run_id).toBe('rn_ok'); // 索引也留着 attempt=0,否则「给一个 node_run_id 查历次判决」少一环 const all = gc.listVerdicts(runDir); expect(all.map((r) => r.verdict_id)).toEqual([a.verdict.verdict_id, b.verdict.verdict_id]); expect(all[0].attempt).toBe(0); expect(gc.listVerdicts(runDir, { onlyFailed: true }).map((r) => r.verdict_id)) .toEqual([b.verdict.verdict_id]); expect(gc.listVerdicts(runDir, { nodeRunId: 'rn_bad' }).map((r) => r.verdict_id)) .toEqual([b.verdict.verdict_id]); expect(gc.listVerdicts(runDir, { limit: 1 }).map((r) => r.verdict_id)) .toEqual([b.verdict.verdict_id]); expect(gc.readVerdict(runDir, 'vd-nope')).toBe(null); }); it('没有契约时 runAcceptance 明确报错(而不是判通过)', async () => { const empty = newRunDir(); await expect(gc.runAcceptance({ runDir: empty, targetPath: 'x' })) .rejects.toThrow(gc.ContractValidationError); }); }); // ──────────────────────────────────────────────────────────────── // 生产路径:模板里印的就是这几行命令 // ──────────────────────────────────────────────────────────────── describe('CLI(validator / 产出节点在 bash 里真正调的那一层)', () => { it('put ×3 → validate → run,与模板 tools.md 的命令一致', () => { const runDir = newRunDir(); const bmFile = writeJson(path.join(runDir, 'artifacts', 'goal-benchmark.json'), BENCHMARK); const tdFile = writeJson(path.join(runDir, 'workspace-test-data.json'), TEST_DATA); writeScript(runDir, 'goal-test-script/acceptance.js', ` const fs = require('fs'); const t = JSON.parse(fs.readFileSync(process.env.ACCEPTANCE_TARGET, 'utf8')); if (!t.scores) { console.error('no scores'); process.exit(1); } `); const scriptFile = writeJson(path.join(runDir, 'workspace-script-section.json'), { runner: 'node', path: 'goal-test-script/acceptance.js', timeout_ms: 20000, }); // 产出侧:三个节点各写一段(benchmark 用 --ref 走外置大文件) expect(runCli(['put', '--run-dir', runDir, '--section', 'benchmark', '--ref', 'goal-benchmark.json', '--goal-id', 'GOAL-cli', '--created-by', 'goal-benchmark']).code).toBe(0); expect(runCli(['put', '--run-dir', runDir, '--section', 'test_data', '--file', tdFile, '--created-by', 'goal-test-data']).code).toBe(0); const put3 = runCli(['put', '--run-dir', runDir, '--section', 'test_script', '--file', scriptFile, '--created-by', 'goal-test-script']); expect(put3.code).toBe(0); expect(put3.stdout).toMatch(/sections: benchmark, test_data, test_script/); expect(fs.existsSync(bmFile)).toBe(true); // planner-validator 侧:只校验契约本身 const valid = runCli(['validate', '--run-dir', runDir]); expect(valid.code).toBe(0); expect(valid.stdout).toMatch(/契约合法/); // org-validator / coder-validator 侧:跑契约。run-dir / node-run-id / attempt 都从 target 推 const good = writeTarget(runDir, 'rn_cli', 3, { scores: { coverage: 92, readability: 85 }, results: { 'c-normal': { out: 4 }, 'c-edge': { out: 0 } }, }); const pass = runCli(['run', '--target', good, '--run-id', 'run_cli']); expect(pass.code).toBe(0); expect(pass.stdout).toMatch(/VERDICT pass/); const bad = writeTarget(runDir, 'rn_cli', 4, { scores: { coverage: 10, readability: 10 }, results: {}, }); const fail = runCli(['run', '--target', bad, '--run-id', 'run_cli']); expect(fail.code).toBe(1); // 1 = 判决为 fail,不是工具坏了 expect(fail.stdout).toMatch(/VERDICT fail\s+fail_reason=acceptance_not_met/); // 判决落库可查,且 attempt 是从 target 文件名推出来的 const rows = gc.listVerdicts(runDir, { nodeRunId: 'rn_cli' }); expect(rows.map((r) => r.attempt)).toEqual([3, 4]); expect(rows.map((r) => r.ok)).toEqual([true, false]); expect(runCli(['list', '--run-dir', runDir, '--only-failed']).stdout).toMatch(/fail/); expect(runCli(['show', '--run-dir', runDir, '--verdict-id', rows[0].verdict_id]).code).toBe(0); }, 30000); it('契约非法 / 没契约 → 退出码 2(与「判决 fail」的 1 区分开)', () => { const runDir = newRunDir(); // 没契约 expect(runCli(['validate', '--run-dir', runDir]).code).toBe(2); // 契约在但非法(绕过 writeContract 直接落一份空契约,模拟产出节点写坏) writeJson(gc.resolveContractPath(runDir), { version: '1', goal_id: 'G' }); const r = runCli(['validate', '--run-dir', runDir]); expect(r.code).toBe(1); expect(r.stdout).toMatch(/input_underspecified/); // 参数错 expect(runCli(['run', '--run-dir', runDir]).code).toBe(2); expect(runCli(['nosuchcmd']).code).toBe(2); }, 20000); it('put 的输出里带 graph_files 行(登记成不成功 agent 都看得见)', () => { const { runDir, companyId, taskId } = newTaskRunDir(); const out = runCli(['put', '--run-dir', runDir, '--section', 'test_data', '--file', writeJson(path.join(runDir, 'td.json'), TEST_DATA), '--goal-id', 'GOAL-cli-idx']); expect(out.code).toBe(0); expect(out.stdout).toContain(path.join(gc.companyRootDir(companyId), 'tasks', taskId, 'acceptance.json')); expect(out.stdout).toMatch(new RegExp(`graph_files: gf_\\w+\\s+kind=acceptance\\s+rel=tasks/${taskId}/acceptance.json`)); }, 20000); it('deriveRunDir / deriveTargetIds:agent 只有产物路径时也能定位', () => { const runDir = newRunDir(); gc.writeContract(runDir, { version: '1', goal_id: 'G', test_data: TEST_DATA }); const target = writeTarget(runDir, 'rn_d', 7, {}); expect(gc.deriveRunDir(target)).toBe(path.resolve(runDir)); expect(gc.deriveTargetIds(target)).toEqual({ nodeRunId: 'rn_d', attempt: 7 }); expect(gc.deriveRunDir(path.join(os.tmpdir(), 'nowhere', 'x.json'))).toBe(null); }); }); // ──────────────────────────────────────────────────────────────── // R19-B:契约落点归并到 tasks/{taskId}/ + 进产物索引 + 旧路径兜底 // 走完四环:产生(写文件)→ 传递(rel_path 相对公司根)→ 落库(graph_files) // → 读回(按 TASK_SHARED_KINDS 查出来,拼回绝对路径必须指到那个文件) // ──────────────────────────────────────────────────────────────── describe('R19-B 契约落点与产物索引', () => { /** graph-engine.describeInputs 就是这么查任务级共享产物的(TASK_SHARED_KINDS) */ function sharedFiles(companyId, taskId) { return wgdb.findFiles({ companyId, taskId, kinds: ['acceptance', 'contract'] }); } it('契约落在 {companyRoot}/tasks/{taskId}/acceptance.json,不再落 runs/{r}/artifacts/', () => { const { runDir, companyId, taskId } = newTaskRunDir(); const expected = path.join(gc.companyRootDir(companyId), 'tasks', taskId, 'acceptance.json'); expect(gc.resolveContractPath(runDir)).toBe(expected); const w = gc.writeContract(runDir, { version: '1', goal_id: 'GOAL-taskdir', test_data: TEST_DATA }); expect(w.path).toBe(expected); expect(fs.existsSync(expected)).toBe(true); // 旧落点一个字都不写(否则两份契约并存,谁是真的说不清) expect(fs.existsSync(path.join(runDir, 'artifacts', 'acceptance.json'))).toBe(false); // 判决**不**跟着搬:它是 run 级事实 expect(gc.resolveVerdictsDir(runDir)).toBe(path.join(runDir, 'artifacts', 'verdicts')); }); it('四环:写契约 → graph_files 有 kind=acceptance 的行 → 拼回绝对路径指到那份文件', () => { const { runDir, companyId, taskId } = newTaskRunDir(); const w = gc.writeContract(runDir, { version: '1', goal_id: 'GOAL-index', test_data: TEST_DATA }); expect(w.indexed).toMatch(/^gf_/); expect(w.index_error).toBeUndefined(); const rows = sharedFiles(companyId, taskId); expect(rows.length).toBe(1); expect(rows[0]).toMatchObject({ kind: 'acceptance', task_id: taskId, company_id: companyId }); expect(rows[0].rel_path).toBe(path.join('tasks', taskId, 'acceptance.json')); expect(rows[0].superseded_by).toBe(null); // 读回这一环:引擎拼绝对路径的方式就是 join(companyRootDir, rel_path) const abs = path.join(gc.companyRootDir(companyId), rows[0].rel_path); expect(abs).toBe(w.path); expect(JSON.parse(fs.readFileSync(abs, 'utf8')).goal_id).toBe('GOAL-index'); // 再写一次(三个产出节点会先后 put):仍然只有一行是当前版本,旧行进版本链 gc.writeContractSection(runDir, { section: 'benchmark', content: BENCHMARK }); const after = sharedFiles(companyId, taskId); expect(after.length).toBe(1); expect(after[0].id).not.toBe(rows[0].id); }); it('存量兼容:旧路径上的契约仍读得到,第一次 put 会把它搬到新路径而不是盖掉', () => { const { runDir, companyId, taskId } = newTaskRunDir(); const legacy = path.join(runDir, 'artifacts', 'acceptance.json'); // R19-B 之前 goal-benchmark 已经把 benchmark 段写在旧路径上了 writeJson(legacy, { version: '1', goal_id: 'GOAL-legacy', created_at: '2026-01-01T00:00:00.000Z', created_by: { benchmark: 'goal-benchmark' }, benchmark: BENCHMARK, }); expect(gc.locateContract(runDir)).toBe(legacy); expect(gc.readContract(runDir).contract.goal_id).toBe('GOAL-legacy'); // 兜底读到 // test-data 节点接着 put 自己那段:合并的 base 必须是旧路径那份 const r = gc.writeContractSection(runDir, { section: 'test_data', content: TEST_DATA, createdBy: 'goal-test-data', }); expect(r.sections).toEqual(['benchmark', 'test_data']); expect(r.path).toBe(path.join(gc.companyRootDir(companyId), 'tasks', taskId, 'acceptance.json')); const now = gc.readContract(runDir); expect(now.path).toBe(r.path); // 新路径优先 expect(now.contract.benchmark).toEqual(BENCHMARK); // 旧段没被抹掉 expect(now.contract.created_at).toBe('2026-01-01T00:00:00.000Z'); expect(sharedFiles(companyId, taskId)[0].rel_path).toBe(path.join('tasks', taskId, 'acceptance.json')); }); it('$ref / test_script.path 仍认旧的 artifacts/(模板照旧教 agent 写那里),逃逸照旧拒绝', async () => { const { runDir } = newTaskRunDir(); // 模板里写的就是 `$R/artifacts/goal-benchmark.json` 与 `$R/artifacts/goal-test-script/*.js` writeJson(path.join(runDir, 'artifacts', 'goal-benchmark.json'), BENCHMARK); writeScript(runDir, 'goal-test-script/acceptance.js', 'process.exit(0)\n'); const w = gc.writeContract(runDir, { version: '1', goal_id: 'GOAL-refs', benchmark: { $ref: 'goal-benchmark.json' }, test_script: { runner: 'node', path: 'goal-test-script/acceptance.js', timeout_ms: 20000 }, }); expect(gc.readContract(runDir).contract.benchmark).toEqual(BENCHMARK); const target = writeTarget(runDir, 'rn_refs', 1, { scores: { coverage: 92, readability: 85 } }); const v = await gc.executeContract(gc.readContract(runDir).contract, target, { runDir }); expect(v.sections.test_script.exit_code).toBe(0); expect(v.ok).toBe(true); expect(w.contract_hash).toBe(v.contract_hash); // 逃逸仍然挡死:多了一个根不等于多了一条逃逸路子(校验层先拒,执行层再兜一次) expect(gc.validateContract({ version: '1', goal_id: 'G', test_script: { runner: 'node', path: '../../../../etc/passwd' }, }).errors.map((e) => e.rule)).toContain('reference'); const missing = await gc.executeContract({ version: '1', goal_id: 'G', test_script: { runner: 'node', path: 'goal-test-script/nope.js' }, }, target, { runDir }); expect(missing.sections.test_script.reason).toBe('script_missing'); // 两个根都没有时报错指向新根(契约目录),不指向已经不是主场的旧 artifacts/ expect(missing.sections.test_script.detail) .toBe(path.join(gc.resolveContractDir(runDir), 'goal-test-script/nope.js')); }, 20000); it('deriveRunDir 靠 task-goal.json 认路(契约已经不在 run 目录里了)', async () => { const { runDir } = newTaskRunDir(); gc.writeContract(runDir, { version: '1', goal_id: 'GOAL-derive', test_data: TEST_DATA }); // run 目录下已经没有 acceptance.json,只剩身份卡 expect(fs.existsSync(path.join(runDir, 'artifacts', 'acceptance.json'))).toBe(false); const target = writeTarget(runDir, 'rn_derive', 2, { results: { 'c-normal': { out: 4 }, 'c-edge': { out: 0 } } }); expect(gc.deriveRunDir(target)).toBe(path.resolve(runDir)); // coder-validator / org-validator 印的就是这行:不带 --run-dir const pass = runCli(['run', '--target', target, '--run-id', 'run_derive']); expect(pass.code).toBe(0); expect(pass.stdout).toMatch(/VERDICT pass/); expect(gc.listVerdicts(runDir, { runId: 'run_derive' }).map((r) => r.attempt)).toEqual([2]); }, 20000); it('推不出 companyId/taskId(旧 run / 手搓目录)时退回旧路径,且不假登记', () => { const runDir = newRunDir(); // 没有 task-goal.json expect(gc.resolveTaskDir(runDir)).toBe(null); const w = gc.writeContract(runDir, { version: '1', goal_id: 'GOAL-nogoal', test_data: TEST_DATA }); expect(w.path).toBe(path.join(runDir, 'artifacts', 'acceptance.json')); expect(w.indexed).toBe(null); expect(w.index_skipped).toMatch(/task-goal\.json/); }); it('companyRootDir / taskDirRel 与 graph-engine 的那两个同构(镜像漂了就报警)', () => { // 两份实现必须算出同一个字符串,否则 graph_files.rel_path 与 describeInputs // 拼出来的绝对路径对不上。镜像已按本用例原先预告的升级路径收拢到 // server/services/company-paths.js(控制台路由成了第三个调用方,靠注释维持 // 「两份逐字同构」的约定就不成立了),所以这里从钉源码文本改成直接比函数返回值 —— // 真正要防的是「两个调用方算出不同的根」,比字符串比源码更接近这件事本身。 const paths = require('../../services/company-paths'); expect(gc.companyRootDir).toBe(paths.companyRootDir); expect(gc.taskDirRel).toBe(paths.taskDirRel); for (const cid of ['co-x', 'co-y']) { expect(gc.companyRootDir(cid)).toBe(paths.companyRootDir(cid)); } expect(gc.companyRootDir('co-x')).toBe(path.join(ENV_ROOT, '_companies', 'co-x')); expect(gc.taskDirRel('t-1')).toBe(path.join('tasks', 't-1')); expect(gc.taskDirRel(null)).toBe(path.join('tasks', '_no-task')); }); });