seed-org-node-guard.test.js
6.91 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
131
132
133
134
/**
* Unit Tests — 补种不许把「图基础设施本体」塞进 org_graph_nodes(28 三轮复验,问题 2)
*
* 锁的是一次**守卫漂移**:`company-service.decideOrgNodeRecording` 的判据①在 P3B/W7 从
* `{'meta-bootstrap'}` 放宽到 `{'meta-bootstrap','meta-planning'}`(要盖住按需雇的
* workgraph-planning,以及它自己雇的 prd/sdd/brd-planning、goal-benchmark),
* 而 `graph-seed-service` 里那份**手抄**的 `isMetaBootstrap()` 停在只认 meta-bootstrap。
* 于是 `seedCompanyAgents()` 把 hire() 特意排除掉的规划本体重新塞回 org_graph_nodes ——
* `authored_by=null` 的孤儿节点,正是 graph-engine.resolvePlanningAgentName 注释里点名要防的
* 「谁也点不亮的 subgraph 节点」;而 work-graph-db 起 org run 时读的就是这张表的 active 行,
* 多出来的是一个**真的会被执行**的 org 节点。
*
* 最坏的一半在对账:`verifyCounts()` 用**同一个**手抄函数算期望值 —— 期望和实际
* 一起错,`npm run graph:seed` exit 0、全程零告警。所以本文件的三条用例缺一不可:
* ① 节点没被写进去(真实症状);
* ② 对账的期望值跟着一致(否则「修好了 seed、对账反而变红」,或者反过来一起错还是绿);
* ③ 存量孤儿节点被 reconcile 清掉且幂等(库里已经有 5 行,光修 seed 不会自己消失)。
* 外加一条漂移守卫:判据的取值域只能有一份。
*
* 隔离手法同 graph-seed-meta-bootstrap.test.js:GRAPH_SEED_ROOT 指向临时目录自建最小夹具,
* GRAPH_DB_PATH 指向一次性 sqlite,完全不碰真实仓库与生产库。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
const MODULES = [
'../../services/graph-db',
'../../services/graph-service',
'../../services/graph-seed-service',
];
describe('seedCompanyAgents × 图基础设施本体守卫', () => {
let seed;
let root;
const COMPANY = 'co-guard';
const ORG_ROLE = 'org-planner'; // type='executor' —— 正经 org 节点
const BOOTSTRAP_ROLE = 'orggraph-planning'; // type='meta-bootstrap' —— 老守卫就认识
const PLANNING_ROLE = 'workgraph-planning'; // type='meta-planning' —— W7 放宽后才该被拦
const nodeRoles = () =>
require('../../services/graph-db').getDb()
.prepare(`SELECT role_name FROM org_graph_nodes WHERE company_id = ? ORDER BY role_name`)
.all(COMPANY)
.map((r) => r.role_name);
beforeAll(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-org-guard-'));
for (const [id, type] of [
[ORG_ROLE, 'executor'],
[BOOTSTRAP_ROLE, 'meta-bootstrap'],
[PLANNING_ROLE, 'meta-planning'],
]) {
const d = path.join(root, 'system', 'agents', id);
fs.mkdirSync(d, { recursive: true });
fs.writeFileSync(path.join(d, 'agent.md'), `# ${id}\n`);
fs.writeFileSync(path.join(d, 'config.json'), JSON.stringify({ type }));
}
fs.writeFileSync(path.join(root, 'system', 'agents', '_registry.json'), JSON.stringify({}));
const companyDir = path.join(root, 'data', 'companies', COMPANY);
fs.mkdirSync(companyDir, { recursive: true });
fs.writeFileSync(
path.join(companyDir, 'company.json'),
JSON.stringify({ id: COMPANY, name: COMPANY, agents: [ORG_ROLE, BOOTSTRAP_ROLE, PLANNING_ROLE] })
);
process.env.GRAPH_SEED_ROOT = root;
process.env.GRAPH_DB_PATH = path.join(root, 'graph.sqlite');
for (const m of MODULES) delete require.cache[require.resolve(m)];
seed = require('../../services/graph-seed-service');
seed.seedAgents();
seed.seedCompanyAgents();
});
afterAll(() => {
delete process.env.GRAPH_SEED_ROOT;
delete process.env.GRAPH_DB_PATH;
try { fs.rmSync(root, { recursive: true, force: true }); } catch {}
for (const m of MODULES) delete require.cache[require.resolve(m)];
});
it('① meta-planning 本体不进 org_graph_nodes(meta-bootstrap 早就不进)', () => {
// 守卫只认 meta-bootstrap 时这里是 ['org-planner', 'workgraph-planning']。
// 两侧都断言:只断言「没有 PLANNING_ROLE」的话,夹具一行都没种进去也会“通过”。
expect(nodeRoles()).toEqual([ORG_ROLE]);
});
it('② verifyCounts() 的期望值跟着一致 —— 两边一致地错才是最难发现的那种', () => {
const v = seed.verifyCounts();
expect(v.mismatches).toEqual([]);
expect(v.ok).toBe(true);
// 显式钉住期望值本身:光看 mismatches 为空,分不清「两边都对」和「两边一致地错」。
expect(v.expected.nodesPerCompany[COMPANY]).toBe(1);
});
it('③ 库里已有的孤儿节点被 reconcile 清掉,且幂等', () => {
const db = require('../../services/graph-db').getDb();
// 复刻漂移期间留下的存量行:authored_by IS NULL(hire 写 'hire'、writeGraph 写 'planner',
// 只有补种脚本两个都不写)。生产库里 mycompany 就有 5 行这样的节点。
db.prepare(`
INSERT INTO org_graph_nodes (company_id, system_agent_id, role_name, status, created_at)
VALUES (?, ?, ?, 'active', ?)
`).run(COMPANY, PLANNING_ROLE, PLANNING_ROLE, new Date().toISOString());
expect(nodeRoles()).toEqual([ORG_ROLE, PLANNING_ROLE]);
const r = seed.reconcileDanglingRows();
expect(r.orphanNonOrgNodesRemoved).toEqual([`${COMPANY}/${PLANNING_ROLE}`]);
expect(nodeRoles()).toEqual([ORG_ROLE]);
// 清完之后对账仍然干净(清理与期望值是同一条判据算出来的)
expect(seed.verifyCounts().mismatches).toEqual([]);
expect(seed.reconcileDanglingRows().orphanNonOrgNodesRemoved).toEqual([]);
});
it('④ 漂移守卫:取值域只有一份 —— company-service 的字面量必须与 graph-service 一致', () => {
// 判据的持有者是 graph-service.NON_ORG_AGENT_TYPES;company-service 眼下还留着一份
// 同名字面量(那是本轮之外的文件),所以这里直接读它的源码比对。
// 这条断言变红有两种正当读法:要么两边真的漂了,要么 company-service 已经改成
// `require('./graph-service').NON_ORG_AGENT_TYPES` —— 后者正是我们想要的终点,
// 那时把这条用例删掉即可。两种都必须有人看见,所以不能只靠注释提醒。
const src = fs.readFileSync(path.resolve(__dirname, '../../services/company-service.js'), 'utf-8');
const m = src.match(/const NON_ORG_AGENT_TYPES = new Set\(\[([^\]]*)\]\)/);
expect(m, 'company-service 里找不到 NON_ORG_AGENT_TYPES 的字面量了,请核对它是否已改为引用 graph-service').not.toBeNull();
const companyServiceTypes = [...m[1].matchAll(/'([^']+)'/g)].map((x) => x[1]).sort();
const graphServiceTypes = [...require('../../services/graph-service').NON_ORG_AGENT_TYPES].sort();
expect(companyServiceTypes).toEqual(graphServiceTypes);
});
});