/** * 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 }); });