graph-boundary.test.js
4.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
/**
* Unit Tests — graph-engine 的项目根与边界守卫(27-P3B W2/W3)
*
* 验的是「产生 → 传递 → 落库 → 读回」四环:
* W2-1 起一个 run,节点收到的任务描述里带着项目根的绝对路径(传递)
* W2-2 项目根落到 runs/{rootRunId}/artifacts/project-root.json(落库)
* W2-3 同一个 root run 的第二个节点读回**同一个**项目根,不重新分配(读回,幂等)
* W3-1 diffRepoStatus 只报「本次调用期间新出现」的那几行(守卫的判据本身)
*
* 隔离手法同 graph-engine.test.js:先设 GRAPH_DB_PATH / GRAPH_RUNS_DIR 再 require,
* 仓库 data/ 全程不被碰;agent 调用走注入的假 executor,不起 Claude CLI。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
describe('graph-engine 项目根与边界守卫', () => {
let wdb;
let engine;
let runsDir;
const TEST_DB_PATH = '/tmp/test-graph-boundary.sqlite';
beforeAll(() => {
for (const f of [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]) {
try { fs.unlinkSync(f); } catch {}
}
process.env.GRAPH_DB_PATH = TEST_DB_PATH;
runsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graph-boundary-runs-'));
process.env.GRAPH_RUNS_DIR = runsDir;
for (const m of [
'../../services/graph-db',
'../../services/graph-service',
'../../services/org-graph-service',
'../../services/work-graph-db',
'../../services/graph-run-sse',
'../../services/graph-engine',
]) {
delete require.cache[require.resolve(m)];
}
wdb = require('../../services/work-graph-db');
engine = require('../../services/graph-engine');
});
afterAll(() => {
for (const f of [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]) {
try { fs.unlinkSync(f); } catch {}
}
delete process.env.GRAPH_DB_PATH;
try { fs.rmSync(runsDir, { recursive: true, force: true }); } catch {}
delete process.env.GRAPH_RUNS_DIR;
// 同一个进程里后续文件再 require 时要拿回没有 override 的那份
for (const m of ['../../services/graph-run-sse', '../../services/graph-engine']) {
delete require.cache[require.resolve(m)];
}
});
function ensureAgents(ids) {
const db = wdb.getDb();
const ts = new Date().toISOString();
const ins = db.prepare(`
INSERT OR IGNORE INTO system_agents
(id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)
`);
for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, ts, ts);
}
it('W2:项目根进提示词、落库到 artifacts/project-root.json,第二个节点读回同一个', async () => {
const companyId = 'co-boundary-1';
ensureAgents(['impl', 'check']);
const g = wdb.createWorkGraph({
companyId,
nodes: [
{ nodeName: 'impl', systemAgentId: 'impl' },
{ nodeName: 'check', systemAgentId: 'check' },
],
edges: [{ from: 'impl', to: 'check', condition: 'success' }],
});
expect(g.errors || []).toEqual([]);
const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId });
engine.markRunReady(run.id);
const tasks = [];
engine.setAgentExecutor(async (ctx) => {
tasks.push({ node: ctx.nodeName, task: ctx.task, projectRoot: ctx.projectRoot });
return { status: 'success' };
});
engine.startRun(run.id, { mode: 'manual' });
await engine.advanceOne(run.id);
await engine.advanceOne(run.id);
engine.setAgentExecutor(null);
// ① 落库
const recFile = path.join(runsDir, run.id, 'artifacts', 'project-root.json');
expect(fs.existsSync(recFile)).toBe(true);
const rec = JSON.parse(fs.readFileSync(recFile, 'utf8'));
expect(rec.root_path).toBeTruthy();
expect(fs.existsSync(rec.root_path)).toBe(true); // 目录真的被建出来了
// ② 传递:两个节点都拿到了项目根,且提示词里是绝对路径
expect(tasks.length).toBe(2);
for (const t of tasks) {
expect(t.projectRoot).toBe(rec.root_path);
expect(t.task).toContain(rec.root_path);
expect(t.task).toContain('[写入边界');
}
// ③ 读回幂等:第二个节点没有另分配一个根
expect(tasks[0].projectRoot).toBe(tasks[1].projectRoot);
});
it('W3:diffRepoStatus 只报本次调用期间新出现的仓库改动', () => {
const before = ' M server/services/graph-engine.js\n?? scratch/a.txt\n';
const after = ' M server/services/graph-engine.js\n?? scratch/a.txt\n?? server/routes/echo.js\n M server/index.js\n';
expect(engine.diffRepoStatus(before, after)).toEqual([
'?? server/routes/echo.js',
'M server/index.js',
]);
// 没有新增就是没有越界;空快照也不该炸
expect(engine.diffRepoStatus(after, after)).toEqual([]);
expect(engine.diffRepoStatus(null, '')).toEqual([]);
});
});