graph-engine-node-outcome-memory.test.js
8.01 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
/**
* Unit Tests — 图执行结果桥接进 memory.md(36 号文档:Innovator/Dream 训练闭环可行性调研)
*
* 36 号文档实查发现的头号阻塞项:`graph-engine.js` 从不写 `memory.md`(唯一写入路径是
* 人类对话 `agent-chat.js`),导致 Dream 每日扫描时,org/work graph 里真正干活的专业节点
* (`prd-planning`/`coder`/`benchmark-validator`……)的 memory.md 永远是模板空壳——不是
* "原料脏",是压根没有原料。本文件锁的是补上的桥接点(`runClaimedNode` 结算之后调用
* `recordNodeOutcomeMemory`):
* ① 真实派发 + 本体已雇佣(bodyDir 存在)+ fail(body_not_ready) → 写一行摘要
* (借道 body_not_ready 端到端验证整条 isRealDispatch 真实链路——它跟其它"真派发"
* 场景一样满足 agentExecutor===defaultAgentExecutor,却在真正调用 CLI 之前短路,
* 是全仓库唯一能不起真 CLI 就摸到 isRealDispatch===true 的路径,35 号文档同款手法)
* ② 本体已雇佣,但 bodyDir 不存在 → 不写,避免测试夹具/未雇佣本体把假地址灌进
* 真实 system/agents/ 目录(与 missingBody 同一条判据,多一层防线)
* ③ 注入假 executor(全仓库其余用例的常态)→ 不写,不干扰任何现有测试
* ④ `recordNodeOutcomeMemory` 直调:success 一行的格式化(advanceOne 端到端摸不到
* 这条分支——isRealDispatch 为真时 else 分支会走真实 defaultAgentExecutor,起真 CLI,
* 不适合单测;直调覆盖纯格式化逻辑,属于"最小可运行检查")
*
* 隔离手法同 graph-engine-body-not-ready.test.js:monkeypatch 模块级活引用
* (`locator.missingBodyDefinition` / `locator.bodyDir` / `memoryService.appendToMemoryMd`),
* 不碰真实 system/agents/ 目录、不做真实文件 I/O。
*/
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
describe('图执行结果桥接进 memory.md(36 号文档)', () => {
let wdb;
let engine;
let locator;
let memoryService;
let runsDir;
const TEST_DB_PATH = '/tmp/test-graph-engine-node-outcome-memory.sqlite';
let origBodyDir;
let origAppendToMemoryMd;
let calls;
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-node-outcome-memory-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',
'../../services/memory-service',
]) {
delete require.cache[require.resolve(m)];
}
wdb = require('../../services/work-graph-db');
engine = require('../../services/graph-engine');
locator = require('../../services/agent-locator');
memoryService = require('../../services/memory-service');
origBodyDir = locator.bodyDir;
origAppendToMemoryMd = memoryService.appendToMemoryMd;
});
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(() => {
locator.missingBodyDefinition = () => [];
locator.bodyDir = origBodyDir;
memoryService.appendToMemoryMd = origAppendToMemoryMd;
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, nodeName = 'solo' }) {
ensureAgent(systemAgentId);
const g = wdb.createWorkGraph({
companyId, taskId,
nodes: [{ nodeName, 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;
}
function spyMemory() {
calls = [];
memoryService.appendToMemoryMd = (...args) => { calls.push(args); };
}
it('① 真实派发(借道 body_not_ready 短路,不起真 CLI)+ bodyDir 存在 → 记一行摘要', async () => {
spyMemory();
const existingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zz-hired-body-'));
locator.bodyDir = (id) => (id === 'zz-hired-agent-1' ? existingDir : origBodyDir(id));
// 本体判为不完整——不会真的调用 defaultAgentExecutor(body_not_ready 在派发之前短路),
// 但仍然是 isRealDispatch===true 的真实结算路径,settled.node.failReason 是真结算出来的。
locator.missingBodyDefinition = (id) => (id === 'zz-hired-agent-1' ? ['agent.md'] : []);
const runId = buildRun({ companyId: 'co-mem-1', taskId: 'task-mem-1', systemAgentId: 'zz-hired-agent-1', nodeName: '实现节点' });
const out = await engine.advanceOne(runId);
expect(out.ok).toBe(true);
expect(calls.length).toBe(1);
const [addr, task, result] = calls[0];
expect(addr).toBe('co-mem-1:zz-hired-agent-1');
expect(task).toContain('实现节点');
expect(task).toContain('第 1 次运行');
expect(result).toBe('fail: body_not_ready');
fs.rmSync(existingDir, { recursive: true, force: true });
});
it('② bodyDir 不存在(未雇佣/测试夹具)→ 不记,避免灌脏真实 system/agents/', async () => {
spyMemory();
locator.bodyDir = (id) => (id === 'zz-unhired-agent' ? path.join(os.tmpdir(), 'zz-graph-engine-memory-test-does-not-exist') : origBodyDir(id));
locator.missingBodyDefinition = (id) => (id === 'zz-unhired-agent' ? ['agent.md'] : []);
const runId = buildRun({ companyId: 'co-mem-2', taskId: 'task-mem-2', systemAgentId: 'zz-unhired-agent' });
await engine.advanceOne(runId);
expect(calls.length).toBe(0);
});
it('③ 注入假 executor(isRealDispatch=false)→ 不记,不干扰全仓库其余用例', async () => {
spyMemory();
const existingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zz-hired-body-fake-exec-'));
locator.bodyDir = (id) => (id === 'zz-fake-exec-agent' ? existingDir : origBodyDir(id));
locator.missingBodyDefinition = () => ['agent.md']; // 就算判成缺失也不该拦——闸门本身就不该生效
engine.setAgentExecutor(async () => ({ status: 'success' }));
const runId = buildRun({ companyId: 'co-mem-3', taskId: 'task-mem-3', systemAgentId: 'zz-fake-exec-agent' });
await engine.advanceOne(runId);
expect(calls.length).toBe(0);
fs.rmSync(existingDir, { recursive: true, force: true });
});
it('④ recordNodeOutcomeMemory 直调:success 一行格式化正确', () => {
spyMemory();
engine.recordNodeOutcomeMemory(
{ companyId: 'co-mem-4', systemAgentId: 'zz-direct-agent', nodeName: '校验节点', attempt: 2 },
{ status: 'success', failReason: null },
);
expect(calls.length).toBe(1);
const [addr, task, result] = calls[0];
expect(addr).toBe('co-mem-4:zz-direct-agent');
expect(task).toContain('校验节点');
expect(task).toContain('第 3 次运行');
expect(result).toBe('success');
});
});