io-model.check.js 5.11 KB
/**
 * 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');