task-decomposition-materialize.test.js 9.01 KB
/**
 * 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
  });
});