graph-engine-body-not-ready.test.js
8.3 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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/**
* Unit Tests — 35 号文档提案 A:本体完整性闸门接进图执行前置检查
*
* `agentLocator.missingBodyDefinition()` 此前只在招聘/建公司路径上被查过
* (graph-service.js / graph-seed-service.js / company-service.js),图执行的
* 核心路径(`runClaimedNode`)从未查过——本体因任何原因仍是空骨架时,引擎会
* 原样把它当正常本体,派发一次真实调用(真实样本见 34 号文档 §五:`unit-tester`
* 全程顶着 `_base` 骨架跑完了一整条真实任务,产出一份没有任何角色指导支撑的结果)。
*
* 本文件锁的是修完之后的行为:
* ① 本体不完整时,节点直接判 fail(`body_not_ready`),**不派发真实调用**
* (注入的 executor 断言从未被调用)
* ② 不扣预算(没有调用发生,「一次调用一定发生了」这条记账理由不成立)
* ③ 本体完整时行为不变(不动 §三十一/§三十三 号文档已经验过的正常路径)
*
* 隔离:monkeypatch `agent-locator.missingBodyDefinition`(模块级函数,`graph-engine.js`
* 每次调用时都走 `locator.missingBodyDefinition(id)` 这个活引用,不是加载时快照,
* patch 完之后立刻生效,不需要碰真实的 system/agents/ 目录),测试结束还原。
*/
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
describe('graph-engine 本体完整性闸门(35 号文档提案 A)', () => {
let wdb;
let engine;
let orgSvc;
let locator;
let runsDir;
const TEST_DB_PATH = '/tmp/test-graph-engine-body-not-ready.sqlite';
const origMissingBodyDefinition = () => {};
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-body-not-ready-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',
'../../services/agent-locator',
]) {
delete require.cache[require.resolve(m)];
}
wdb = require('../../services/work-graph-db');
engine = require('../../services/graph-engine');
orgSvc = require('../../services/org-graph-service');
locator = require('../../services/agent-locator');
});
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;
for (const m of ['../../services/graph-run-sse', '../../services/graph-engine']) {
delete require.cache[require.resolve(m)];
}
});
afterEach(() => {
// 每条用例自己决定 patch 成什么,用完必须还原——同一个 locator 模块实例
// 会被下一条用例复用(require.cache 只在 beforeAll 清过一次)
locator.missingBodyDefinition = origMissingBodyDefinition;
engine.setAgentExecutor(null);
});
function ensureAgent(id) {
const db = wdb.getDb();
const ts = new Date().toISOString();
db.prepare(`
INSERT OR IGNORE INTO system_agents
(id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)
`).run(id, id, `system/agents/${id}`, ts, ts);
}
function buildRun({ companyId, taskId, systemAgentId }) {
ensureAgent(systemAgentId);
const g = wdb.createWorkGraph({
companyId, taskId,
nodes: [{ nodeName: 'solo', systemAgentId }],
edges: [],
});
expect(g.errors || []).toEqual([]);
const run = wdb.createRun({
graphKind: 'work', graphId: g.id, companyId, taskId,
budget: { total_agent_calls: 10, total_tokens: 5_000_000, spent: { agent_calls: 0, tokens: 0 } },
});
engine.markRunReady(run.id);
engine.startRun(run.id, { mode: 'manual' });
return run.id;
}
// ①②:这道闸门只在**真会起 CLI 的执行器**下生效(与 `boundaryGuardApplicable`
// 同一个判据,见 graph-engine.js 里那条注释)——所以这两条用例**不注入假 executor**,
// 让 `agentExecutor` 保持模块默认值(`=== defaultAgentExecutor`),从而真正触发闸门;
// 又因为闸门在派发之前就短路返回,`defaultAgentExecutor` 实际上从未被调用到,
// 不会真的去起 Claude CLI 子进程。
it('① 本体不完整:节点直接判 fail(body_not_ready),短路在真正派发之前', async () => {
const companyId = 'co-body-not-ready';
const taskId = 'task-body-not-ready';
const runId = buildRun({ companyId, taskId, systemAgentId: 'zz-blank-body' });
locator.missingBodyDefinition = (id) => (id === 'zz-blank-body' ? ['agent.md', 'soul.md'] : []);
const out = await engine.advanceOne(runId);
expect(out.ok).toBe(true);
const state = wdb.getRunState(runId);
expect(state.nodes[0].status).toBe('blocked');
expect(state.nodes[0].failReason).toBe('body_not_ready');
});
it('② 本体不完整时不扣预算——没有调用发生,「一次调用一定发生了」这条记账理由不成立', async () => {
const companyId = 'co-body-not-ready-budget';
const taskId = 'task-body-not-ready-budget';
const runId = buildRun({ companyId, taskId, systemAgentId: 'zz-blank-body-2' });
locator.missingBodyDefinition = () => ['agent.md'];
await engine.advanceOne(runId);
const run = wdb.getRun(runId);
const spent = JSON.parse(run.budget_json).spent;
expect(spent.agent_calls).toBe(0);
});
it('③ 注入假 executor 时这道闸门不生效——保护全仓库其余用「假 systemAgentId + 假 executor」的用例', async () => {
const companyId = 'co-body-ready';
const taskId = 'task-body-ready';
const runId = buildRun({ companyId, taskId, systemAgentId: 'zz-fixture-only-agent' });
// 就算把它判成"缺失",只要走的是注入的假 executor,闸门也不该拦——这正是本改动
// 第一版把全仓库测试炸穿的那个坑(真实文件系统里几乎没有一个测试夹具用的
// systemAgentId 真的对应 system/agents/ 下的目录),修完之后要把它钉住不再复现。
locator.missingBodyDefinition = () => ['agent.md'];
let executorCalled = false;
engine.setAgentExecutor(async () => {
executorCalled = true;
return { status: 'success' };
});
await engine.advanceOne(runId);
expect(executorCalled).toBe(true);
const run = wdb.getRun(runId);
expect(JSON.parse(run.budget_json).spent.agent_calls).toBe(1);
const state = wdb.getRunState(runId);
expect(state.nodes[0].status).toBe('success');
});
it('④ subgraph 节点不受这道闸门约束——它派发的是 workgraph-planning 规划子图,不是本体自己', async () => {
const companyId = 'tmp-body-subgraph';
const taskId = 'task-body-subgraph';
ensureAgent('zz-org-node-agent');
expect(orgSvc.writeGraph(companyId, {
nodes: [{ roleName: 'org-solo', systemAgentId: 'zz-org-node-agent' }],
edges: [],
}).ok).toBe(true);
const run = wdb.createRun({
graphKind: 'org', graphId: companyId, companyId, taskId,
budget: { total_agent_calls: 10, total_tokens: 5_000_000, spent: { agent_calls: 0, tokens: 0 } },
});
engine.markRunReady(run.id);
engine.startRun(run.id, { mode: 'manual' });
// 就算这个"本体"是空骨架,subgraph 节点也不该被这道闸门拦——它不直接调用这个本体,
// 它派发的是 workgraph-planning 去规划子图
locator.missingBodyDefinition = () => ['agent.md'];
let planCalled = false;
engine.setPlanWorkGraph(async () => {
planCalled = true;
return { draft: { nodes: [{ nodeName: 'x', systemAgentId: 'zz-org-node-agent' }], edges: [] }, source: 'test' };
});
try {
await engine.advanceOne(run.id);
expect(planCalled).toBe(true);
} finally {
engine.setPlanWorkGraph(null);
}
});
});