task-decomposition-materialize.test.js
9.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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/**
* 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 |
`;
// 47 号文档:p0verify0901 真实回归跑出来的 goal-benchmark.json 精简版(保留形状,砍掉部分维度)
const GOAL_BENCHMARK_JSON = JSON.stringify({
dimensions: [
{ name: 'direction_control', weight: 0.5, pass_threshold: 80,
rubric: { '0': '方向键无效', '100': '四个方向键都能正确转向' },
evidence_hint: '模拟按键序列断言 state.direction' },
{ name: 'eat_food_grow_score', weight: 0.5, pass_threshold: 80,
rubric: { '0': '吃到食物无变化', '100': '长度和分数同步增加' },
evidence_hint: '断言 snake.length 与 score 同步变化' },
],
overall_pass_threshold: 80,
traceability: [
{ task_goal_item: '①方向键控制', covered_by: ['direction_control'] },
{ task_goal_item: '②吃到食物加分', covered_by: ['eat_food_grow_score'] },
],
});
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, writeGoalBenchmark = false } = {}) {
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);
if (writeGoalBenchmark) {
const artifactsDir = path.join(runsDir, ctx.run.root_run_id, 'artifacts');
fs.mkdirSync(artifactsDir, { recursive: true });
fs.writeFileSync(path.join(artifactsDir, 'goal-benchmark.json'), GOAL_BENCHMARK_JSON);
}
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
});
it('47 号文档:acceptance.md 不存在但 goal-benchmark.json 存在(共享 artifacts 区)→ 按维度物化子任务', async () => {
const { task } = await setupAndRunPlanner('co-materialize-4', { writeFile: false, writeGoalBenchmark: true });
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('①方向键控制'); // 优先取 traceability 的原始需求条目,不是维度英文名
expect(children[0].acceptance_mode).toBe('holistic');
expect(children[0].description).toContain('模拟按键序列断言 state.direction');
expect(children[0].description).toContain('pass_threshold=80');
expect(children[1].title).toBe('②吃到食物加分');
});
it('acceptance.md 优先于 goal-benchmark.json:两者都存在时不走兜底', async () => {
const { task } = await setupAndRunPlanner('co-materialize-5', { writeFile: true, writeGoalBenchmark: true });
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('方向键控制移动'); // 来自 ACCEPTANCE_MD,不是 GOAL_BENCHMARK_JSON
});
});