/** * io-model 的可运行检查(`node web/src/components/graph-console/io-model.check.js`)。 * * 根 vitest.config.js 的 include 不覆盖 web/,所以这里用 assert 自跑,不引框架。 * 盯的是「契约 × 实例」那次 join 的每种真实形态:解析到 / 必需缺失(第四次真跑的断点形态)/ * 可选缺失(引擎根本不会拼进提示词)/ 非相邻的任务级共享 / 中继(实例不是紧邻上游产的)/ * 后端字段缺失(整块降级,不猜)。 */ import assert from 'node:assert/strict'; import { resolveNodeIo, edgeContractLabel, absPath, normFile } from './io-model.js'; const nodes = [ { id: 'rn_brd', defNodeId: 'brd', name: 'brd' }, { id: 'rn_prd', defNodeId: 'prd', name: 'prd' }, { id: 'rn_val', defNodeId: 'validator', name: 'validator' }, ]; const edges = [ { id: 'e1', from: 'brd', to: 'prd', condition: 'success', edgeKind: 'forward', contracts: [{ kind: 'brd', required: 1 }, { kind: 'glossary', required: 0 }] }, { id: 'e2', from: 'prd', to: 'validator', condition: 'success', edgeKind: 'forward', contracts: [{ kind: 'prd', required: 1 }] }, { id: 'e3', from: 'validator', to: 'brd', condition: 'fail', edgeKind: 'backward', contracts: [{ kind: 'prd', required: 1 }] }, ]; const files = [ { id: 'gf1', rel_path: 'tasks/t1/brd.md', kind: 'brd', produced_by_node_run_id: 'rn_brd', created_at: '1' }, { id: 'gf2', rel_path: 'tasks/t1/acceptance.json', kind: 'acceptance', produced_by_node_run_id: 'rn_brd', created_at: '2' }, ]; const companyRoot = '/data/companies/c1'; // ① prd 节点:入边声明 brd(有实例)+ glossary(可选、无实例);acceptance 走任务级共享 const prd = resolveNodeIo({ node: nodes[1], nodes, edges, files, companyRoot }); const brdIn = prd.inputs.find((i) => i.kind === 'brd'); assert.equal(brdIn.resolved, true); assert.equal(brdIn.path, '/data/companies/c1/tasks/t1/brd.md'); assert.deepEqual(brdIn.froms.map((f) => f.fromName), ['brd']); const gloss = prd.inputs.find((i) => i.kind === 'glossary'); assert.equal(gloss.resolved, false); assert.equal(gloss.inPrompt, false, '可选且无实例 → 引擎整行省略,UI 必须照实说 agent 看不到'); assert.equal(prd.missingRequired, 0); const acc = prd.inputs.find((i) => i.kind === 'acceptance'); assert.equal(acc.source, 'task', '非相邻依赖只能按 task+kind 检索,不在任何边上'); // 出边契约:声明要往 validator 传 prd,索引里整个 task 都没有 prd → 标 missing assert.deepEqual(prd.outbound.map((o) => [o.toName, o.contracts[0].kind, o.contracts[0].state]), [['validator', 'prd', 'missing']]); assert.equal(prd.outputs.length, 0); // ② validator 节点:必需输入 prd 没有实例 —— 第四次真跑那个断点的形态 const val = resolveNodeIo({ node: nodes[2], nodes, edges, files, companyRoot }); assert.equal(val.missingRequired, 1); assert.equal(val.inputs.find((i) => i.kind === 'prd').inPrompt, true, '必需缺失要拼 ⚠️ 行给 agent'); assert.equal(val.inputs.filter((i) => i.kind === 'prd').length, 1, 'backward 入边不是数据来源'); // ③ brd 节点:两份产物都登记在它名下 const brd = resolveNodeIo({ node: nodes[0], nodes, edges, files, companyRoot }); assert.deepEqual(brd.outputs.map((o) => o.kind).sort(), ['acceptance', 'brd']); assert.equal(brd.declaredNone, true, '它没有入边契约'); // ④ 后端没补字段:整块降级,不猜 const bare = resolveNodeIo({ node: nodes[1], nodes, edges: [{ id: 'e1', from: 'brd', to: 'prd' }] }); assert.equal(bare.hasIndex, false); assert.deepEqual(bare.inputs, []); // ⑤ role='needs':产出方不必是紧邻上游,所以既不写成「来自 X」,也不拿它标产出方的红 const relayEdges = [ { id: 'r1', from: 'brd', to: 'prd', condition: 'success', edgeKind: 'forward', contracts: [{ kind: 'sdd', required: 1, role: 'needs' }] }, ]; const relay = resolveNodeIo({ node: nodes[1], nodes, edges: relayEdges, files, companyRoot }); assert.equal(relay.inputs[0].relay, true); // ⑥ 中继:实例存在、但不是这条边的上游产的 —— 合法(引擎按 task+kind 查,不看谁产的), // 不能标红。graph_edge_contracts 现在没有 role 列,所以这条判断不能依赖 role,只能看索引。 const midEdges = [ { id: 'm1', from: 'prd', to: 'validator', condition: 'success', edgeKind: 'forward', contracts: [{ kind: 'acceptance', required: 0 }] }, ]; const mid = resolveNodeIo({ node: nodes[1], nodes, edges: midEdges, files, companyRoot }); assert.equal(mid.outbound[0].contracts[0].state, 'elsewhere'); assert.equal(mid.outbound[0].contracts[0].by, 'brd', '要指出实例到底是谁产的'); // ⑦ 边标克制到一枚药丸 assert.equal(edgeContractLabel(edges[0]).text, 'brd +1'); assert.equal(edgeContractLabel(edges[0]).title, 'brd · glossary?'); assert.equal(edgeContractLabel({ id: 'x' }), null); // ⑧ 库里只存相对路径;根没带下来就别编前缀 assert.equal(absPath(null, 'a/b.md'), 'a/b.md'); assert.equal(absPath('/r/', 'a/b.md'), '/r/a/b.md'); assert.equal(normFile({ rel_path: 'x', superseded_by: 'gf9' }).supersededBy, 'gf9'); console.log('io-model.check: ok');