task-decomposition-materialize.test.js
6.55 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
/**
* 43 号文档 Phase 2:org-planner 节点结算 success 后,把 acceptance.md 物化成子任务。
*
* org 层节点全是 subgraph(isSubgraphNode 恒真),真正的产出在 workgraph-planning 展开出的
* 子 run 里——所以这里跟 goal-task-acceptance.test.js 同源,用 `setPlanWorkGraph` 注入假
* 规划(返回一张手造的单节点 work graph),再用 `setAgentExecutor` 注入那个节点的假执行,
* 不真起 Claude CLI。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
const TEST_DB_PATH = '/tmp/test-task-decomp-materialize.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/graph-run-sse',
'../../services/graph-engine',
];
const ORG_NODES = ['org-planner', 'org-coder', 'org-validator'];
const ORG_EDGES = [
{ from: 'org-planner', to: 'org-coder', condition: 'success' },
{ from: 'org-coder', to: 'org-validator', condition: 'success' },
];
const ACCEPTANCE_MD = `# 验收清单
| # | 验收项 | 判定方式 | pass 条件 |
|---|--------|----------|-----------|
| 1 | 方向键控制移动 | 手动操作 | 蛇按方向键移动 |
| 2 | 吃到食物加分 | 手动操作 | 分数 +1 |
`;
describe('43 号文档 Phase 2:acceptance.md → 子任务物化', () => {
let wdb;
let org;
let engine;
let runsDir;
beforeAll(() => {
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(), 'task-decomp-runs-'));
process.env.GRAPH_RUNS_DIR = runsDir;
for (const m of MODULES) delete require.cache[require.resolve(m)];
wdb = require('../../services/work-graph-db');
org = require('../../services/org-graph-service');
engine = require('../../services/graph-engine');
});
afterAll(() => {
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)];
});
function ensureAgent(id) {
const db = wdb.getDb();
const now = 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}`, now, now);
}
/** 建一家公司的 org 图 + 一条 task,起一条 org run,跑到 org-planner(含它展开的子图)结算为止 */
async function setupAndRunPlanner(companyId, { acceptanceMode = 'itemized', writeFile = true } = {}) {
expect(org.writeGraph(companyId, { nodes: ORG_NODES, edges: ORG_EDGES }).ok).toBe(true);
ensureAgent('planner-worker');
const goal = wdb.createGoal({ companyId, title: 'g', description: '贪吃蛇游戏' });
const task = wdb.createTask({
goalId: goal.id, title: '实现贪吃蛇', description: 'd', acceptanceMode,
});
const run = wdb.createRun({ graphKind: 'org', graphId: companyId, companyId, taskId: task.id });
engine.markRunReady(run.id);
engine.startRun(run.id, { mode: 'manual' });
engine.setPlanWorkGraph(async () => {
const g = wdb.createWorkGraph({
companyId, nodes: [{ nodeName: 'planner-worker', systemAgentId: 'planner-worker' }], edges: [],
});
expect(g.errors || []).toEqual([]);
return { workGraphId: g.id };
});
let childRunId;
try {
const planned = await engine.nextStep(run.id); // org-planner:规划出子图,父节点转 running 等子 run
childRunId = planned.node && planned.node.childRunId;
} finally {
engine.setPlanWorkGraph(null);
}
expect(childRunId).toBeTruthy();
engine.setAgentExecutor(async (ctx) => {
const dir = path.join(runsDir, ctx.run.root_run_id, 'nodes', ctx.nodeRunId);
fs.mkdirSync(dir, { recursive: true });
if (writeFile) fs.writeFileSync(path.join(dir, 'acceptance.md'), ACCEPTANCE_MD);
return { status: 'success', outputRef: dir };
});
try {
await engine.advanceOne(childRunId); // 子图里唯一的那个节点
} finally {
engine.setAgentExecutor(null);
}
return { task, run, childRunId };
}
it('itemized + acceptance.md 存在 → org-planner 结算 success 后按表格行数物化子任务', async () => {
const { task, run } = await setupAndRunPlanner('co-materialize-1');
const plannerNode = wdb.getDb().prepare(`
SELECT status FROM graph_run_nodes WHERE run_id = ?
AND def_node_id = (SELECT id FROM org_graph_nodes WHERE company_id='co-materialize-1' AND role_name='org-planner')
`).get(run.id);
expect(plannerNode.status).toBe('success'); // 前提:子图真的收敛回了父节点
const children = wdb.getDb()
.prepare(`SELECT * FROM tasks WHERE parent_task_id = ? ORDER BY created_at`)
.all(task.id);
expect(children).toHaveLength(2);
expect(children[0].title).toBe('方向键控制移动');
expect(children[0].acceptance_mode).toBe('holistic');
expect(children[0].description).toContain('手动操作');
expect(children[0].description).toContain('蛇按方向键移动');
expect(children[1].title).toBe('吃到食物加分');
});
it('holistic 任务:org-planner 成功结算,不物化任何子任务', async () => {
const { task } = await setupAndRunPlanner('co-materialize-2', { acceptanceMode: 'holistic' });
const n = wdb.getDb().prepare(`SELECT COUNT(*) AS n FROM tasks WHERE parent_task_id = ?`).get(task.id).n;
expect(n).toBe(0);
});
it('itemized 但 acceptance.md 不存在:org-planner 仍判 success,不物化子任务(软失败,不拖累主产出)', async () => {
const { task, run } = await setupAndRunPlanner('co-materialize-3', { writeFile: false });
const n = wdb.getDb().prepare(`SELECT COUNT(*) AS n FROM tasks WHERE parent_task_id = ?`).get(task.id).n;
expect(n).toBe(0);
const plannerNode = wdb.getDb().prepare(`
SELECT status FROM graph_run_nodes WHERE run_id = ?
AND def_node_id = (SELECT id FROM org_graph_nodes WHERE company_id='co-materialize-3' AND role_name='org-planner')
`).get(run.id);
expect(plannerNode.status).toBe('success'); // 找不到 acceptance.md 不该把这一步打成 fail
});
});