graph-run-route-next-auto-guard.test.js
5.81 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
/**
* Unit Test — POST /api/graph-runs/:id/next 必须拒绝 auto 模式的 run(T-mtgxcnfz 复现)
*
* `execution_mode='auto'` 的 run 在 `startRun` 里已经挂了一个 `runAuto` 后台驱动循环
* (graph-engine.js 的 `runAuto`),会自己不停调 `advanceTree` 往下推。此前 `/next` 路由
* 只检查 `run_status ∈ {running, paused}`,不看 `execution_mode`——人在控制台上对一条正在
* auto 跑的 run 手动点「下一步」,会和后台驱动循环并发推进同一条 run,两条路径互不知情。
*
* 真实事故:CEO 验证 44 号修复时手动反复调 /next 撞上这个口子,导致 task/goal 被误判
* cancelled/failed,人工排查+纠正过数据(见 T-mtgxcnfz 任务描述)。
*
* 走真入口:router 挂真 express,listen(0) 再 fetch(与 graph-run-route-io.test.js 同法)。
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
const express = require('express');
const TEST_DB_PATH = '/tmp/test-graph-run-route-next-auto-guard.sqlite';
const DB_FILES = [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`];
const MODULES = [
'../../services/graph-db',
'../../services/graph-service',
'../../services/org-graph-service',
'../../services/work-graph-db',
'../../services/company-paths',
'../../services/graph-run-sse',
'../../services/graph-engine',
'../../routes/graph-run',
];
const CID = 'co-next-auto-guard';
describe('POST /api/graph-runs/:id/next — auto 模式必须 400,不许和后台驱动并发推进', () => {
let wdb;
let engine;
let srv;
let base;
let runsDir;
beforeAll(async () => {
require('../helpers/ensure-test-company').ensureTestCompany(CID);
for (const f of DB_FILES) { try { fs.unlinkSync(f); } catch {} }
process.env.GRAPH_DB_PATH = TEST_DB_PATH;
runsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'route-next-auto-guard-runs-'));
process.env.GRAPH_RUNS_DIR = runsDir;
for (const m of MODULES) delete require.cache[require.resolve(m)];
wdb = require('../../services/work-graph-db');
engine = require('../../services/graph-engine');
const app = express();
app.use(express.json());
app.use('/api/graph-runs', require('../../routes/graph-run'));
await new Promise((resolve) => {
srv = app.listen(0, '127.0.0.1', () => {
base = `http://127.0.0.1:${srv.address().port}`;
resolve();
});
});
});
afterAll(async () => {
if (srv) await new Promise((r) => srv.close(r));
require('../helpers/ensure-test-company').removeTestCompany(CID);
for (const f of DB_FILES) { 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 MODULES) delete require.cache[require.resolve(m)];
});
afterEach(() => {
engine.setAgentExecutor(null);
});
function ensureAgents(ids) {
const db = wdb.getDb();
const now = new Date().toISOString();
const ins = 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', ?, ?)
`);
for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
}
it('run 处于 running 且 execution_mode=auto 时,/next 返回 400,不推进节点', async () => {
ensureAgents(['w']);
const g = wdb.createWorkGraph({ companyId: CID, nodes: ['w'], edges: [] });
expect(g.errors || []).toEqual([]);
const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId: CID, executionMode: 'auto' });
engine.markRunReady(run.id);
// executor 挂起不返回:让节点卡在 running,run 停在 running + auto,
// 这正是「后台驱动循环还没跑完」的真实窗口——/next 必须在这个窗口里也拒绝。
let releaseNode;
engine.setAgentExecutor(() => new Promise((resolve) => { releaseNode = resolve; }));
const { driver } = engine.startRun(run.id, { mode: 'auto' });
// 等 advanceOne 真正把节点从 ready 推成 running 再发请求,避免时序竞态误判
await new Promise((resolve) => {
const check = () => {
const node = wdb.getRunState(run.id).nodes.find((n) => n.name === 'w');
if (node && node.status === 'running') return resolve();
setImmediate(check);
};
check();
});
const before = wdb.getRun(run.id);
expect(before.execution_mode).toBe('auto');
expect(before.run_status).toBe('running');
const r = await fetch(`${base}/api/graph-runs/${run.id}/next`, { method: 'POST' });
expect(r.status).toBe(400);
const body = await r.json();
expect(body.error).toMatch(/auto/);
// 节点没被 /next 动过——还是同一个 running 节点,没有被拾取第二次
const after = wdb.getRunState(run.id).nodes.find((n) => n.name === 'w');
expect(after.status).toBe('running');
expect(after.attempt).toBe(before && 0); // 未被推进,attempt 维持初始值
releaseNode({ status: 'success' });
await driver;
expect(wdb.getRun(run.id).run_status).toBe('completed');
});
it('manual 模式的 running run,/next 仍然放行(不误伤既有用法)', async () => {
ensureAgents(['m']);
const g = wdb.createWorkGraph({ companyId: CID, nodes: ['m'], edges: [] });
const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId: CID, executionMode: 'manual' });
engine.markRunReady(run.id);
engine.setAgentExecutor(async () => ({ status: 'success' }));
engine.startRun(run.id, { mode: 'manual' });
const r = await fetch(`${base}/api/graph-runs/${run.id}/next`, { method: 'POST' });
expect(r.status).toBe(200);
expect(wdb.getRun(run.id).run_status).toBe('completed');
});
});