template-refs.test.js
5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* Unit Tests — work graph 骨架的悬空本体引用防线(R6-F1)
*
* 挡的是这一类缺陷:`system/work-graph-templates/*.json` 里写了一个 `systemAgentId`,
* 而 `system/agents/` 下根本没有这个 agent。它不会在任何静态检查里报错,
* 一路要跑到生产才炸——`work_graph_nodes.system_agent_id` 有外键指向 `system_agents(id)`,
* `createWorkGraph` 落库时抛 `FOREIGN KEY constraint failed`,
* `createWorkGraphSafely` 把它翻成 `rule=reference` 打回 agent,连打三轮后抛错,
* 节点按「无 fail_reason 的 fail」走兜底边。而 `org-validator` 的模板里写死了
* 「必须用 validator.json 这张骨架」,所以这条路径是 100% 会踩的。
*
* 实际发生过:validator.json 引用 benchmark-validator / testdata-validator,
* planner.json 引用 brd-planning / prd-planning / sdd-planning / goal-benchmark,
* 六个全是空引用(R6-F1)。
*
* ── 为什么校验的是「目录」而不只是「_registry.json 条目」──────────────
* `system_agents` 的数据源头是**目录扫描**:`graph-seed-service.seedAgents()`
* 读 `system/agents/*` 的子目录逐个 upsert,`_registry.json` 只用来补充可选的
* `benchmarkScore`(匹配不上就留 NULL)。所以决定外键成不成立的是**目录在不在**。
* 但 registry 是这批 agent 的人类可读清单,漏登记会让 `_registry.json` 与真实本体集
* 悄悄发散——两条都查,一条挡外键、一条挡元数据漂移。
*
* ── 为什么不顺带查 `system/org-graph-templates/` ────────────────────
* 那边的节点是 org node 角色名(`org-planner` / `org-coder` …),
* 它们不是 `system/agents/` 下的本体,而是建公司时由
* `graph-seed-service.seedCompanyAgents()` 以 `origin='created'` 落库的。
* 拿本文件这套判据去查它,会把正确的东西判成错的。
*/
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
const ROOT = path.resolve(__dirname, '../../..');
const WORK_GRAPH_DIR = path.join(ROOT, 'system/work-graph-templates');
const TEMPLATES_DIR = path.join(ROOT, 'system/agents');
const REGISTRY_PATH = path.join(TEMPLATES_DIR, '_registry.json');
/** `{{orgNode}}` 这类占位符不是合法本体名,由使用方替换(README「`{{占位符}}` 必须替换」) */
const isPlaceholder = (s) => /\{\{.*\}\}/.test(s);
function readSkeletons() {
return fs
.readdirSync(WORK_GRAPH_DIR)
.filter((f) => f.endsWith('.json'))
.map((f) => ({ file: f, json: JSON.parse(fs.readFileSync(path.join(WORK_GRAPH_DIR, f), 'utf-8')) }));
}
/**
* 骨架里所有被引用的本体 id。
* `normalizeNodes`(work-graph-db.js)在缺 systemAgentId 时回退到 nodeName——
* 这里照同一条规则取,否则漏掉的正是最容易写错的那种节点。
*/
function referencedAgentIds(skeleton) {
const out = [];
for (const n of skeleton.json.nodes || []) {
const nodeName = typeof n === 'string' ? n : n && (n.nodeName || n.node_name);
const id = (typeof n === 'object' && n && (n.systemAgentId || n.system_agent_id)) || nodeName;
if (id && !isPlaceholder(id)) out.push({ id, nodeName, file: skeleton.file });
}
return out;
}
describe('work graph 骨架的本体引用', () => {
const skeletons = readSkeletons();
it('本目录下有骨架可查(防止测试因目录改名而静默变成空跑)', () => {
expect(skeletons.length).toBeGreaterThan(0);
});
it('每个 systemAgentId 在 system/agents/ 下都有目录(决定外键成不成立的那一环)', () => {
const dangling = [];
for (const s of skeletons) {
for (const ref of referencedAgentIds(s)) {
if (!fs.existsSync(path.join(TEMPLATES_DIR, ref.id))) dangling.push(ref);
}
}
expect(
dangling.map((d) => `${d.file}: 节点 ${d.nodeName} → system/agents/${d.id} 不存在`),
).toEqual([]);
});
it('每个 systemAgentId 在 _registry.json 里都有条目', () => {
const registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8'));
expect(Array.isArray(registry)).toBe(true);
const known = new Set(registry.map((e) => e.id));
const missing = [];
for (const s of skeletons) {
for (const ref of referencedAgentIds(s)) {
if (!known.has(ref.id)) missing.push(ref);
}
}
expect(
missing.map((m) => `${m.file}: 节点 ${m.nodeName} → _registry.json 里没有 ${m.id}`),
).toEqual([]);
});
it('引用的本体确实能落库:真的建一次库并写入无占位符的骨架', async () => {
// 隔离手法与 graph-engine.test.js 一致:先设 GRAPH_DB_PATH 再 import,真实 data/graph.sqlite 不被碰。
const os = await import('os');
const dbPath = path.join(
fs.mkdtempSync(path.join(os.default.tmpdir(), 'template-refs-')),
'graph.sqlite',
);
process.env.GRAPH_DB_PATH = dbPath;
const graphDb = await import('../../services/graph-db.js');
const seed = await import('../../services/graph-seed-service.js');
const wdb = await import('../../services/work-graph-db.js');
graphDb.default.getDb();
seed.default.seedAgents(); // system/agents/* → system_agents(外键的另一端)
const landed = [];
for (const s of skeletons) {
if (JSON.stringify(s.json).includes('{{')) continue; // 占位符骨架落库前必须先替换,不在本条覆盖范围
const res = wdb.default.createWorkGraph({
companyId: 'template-refs-test',
nodes: s.json.nodes,
edges: s.json.edges,
});
landed.push(`${s.file}: ${res.ok ? 'ok' : JSON.stringify(res.errors)}`);
}
// 无占位符的骨架必须至少有一张(validator.json 是 org-validator 强制使用的那张),
// 否则这条用例会在"没有任何图被落库"的情况下静默通过。
expect(landed.length).toBeGreaterThan(0);
expect(landed.filter((l) => !l.endsWith(': ok'))).toEqual([]);
});
});