task-decomposition-validator-gate.test.js 8 KB
/**
 * 43 号文档 Phase 3:coder-validator 判自己 success 之前,子任务必须全部 completed
 * 才认;以及 settleSubtaskResult 这个落库口本身的条件更新/幂等/校验。
 */
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-validator-gate.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',
];

describe('43 号文档 Phase 3', () => {
  let wdb;
  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-gate-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');
  });

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

  // ── settleSubtaskResult 本身 ─────────────────────────────────────

  it('settleSubtaskResult:非法 status 直接拒', () => {
    expect(() => wdb.settleSubtaskResult('T-whatever', 'running')).toThrow(/status 只能是/);
  });

  it('settleSubtaskResult:不是子任务(parent_task_id 为空)不生效,changes=0', () => {
    const goal = wdb.createGoal({ companyId: 'co-gate-1', title: 'g', description: 'd' });
    const top = wdb.createTask({ goalId: goal.id, title: 't', description: 'd' });
    const changed = wdb.settleSubtaskResult(top.id, 'completed');
    expect(changed).toBe(0);
    expect(wdb.getTask(top.id).status).toBe('draft'); // 没被动过
  });

  it('settleSubtaskResult:正常上报 + 终态幂等(第二次不同结果上报不生效)', () => {
    const goal = wdb.createGoal({ companyId: 'co-gate-2', title: 'g', description: 'd' });
    const parent = wdb.createTask({ goalId: goal.id, title: 'p', description: 'd', acceptanceMode: 'itemized' });
    const child = wdb.createTask({
      goalId: goal.id, title: 'c', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
    });

    expect(wdb.settleSubtaskResult(child.id, 'completed', '通过')).toBe(1);
    const after1 = wdb.getTask(child.id);
    expect(after1.status).toBe('completed');
    expect(after1.summary).toBe('通过');
    expect(after1.finished_at).toBeTruthy();

    // 已经是终态,再上报 failed 不该覆盖——防的是 coder-validator 因 retry 重复上报
    expect(wdb.settleSubtaskResult(child.id, 'failed', '改判')).toBe(0);
    expect(wdb.getTask(child.id).status).toBe('completed');
  });

  // ── 引擎侧的闸门:coder-validator 判 success 前核子任务 ─────────────

  async function runValidator(companyId, taskId) {
    ensureAgent('coder-validator');
    const g = wdb.createWorkGraph({
      companyId,
      taskId,
      nodes: [{ nodeName: 'v', systemAgentId: 'coder-validator' }],
      edges: [],
    });
    expect(g.errors || []).toEqual([]);
    const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId, taskId });
    engine.markRunReady(run.id);
    engine.startRun(run.id, { mode: 'manual' });
    engine.setAgentExecutor(async () => ({ status: 'success' }));
    let out;
    try {
      out = await engine.advanceOne(run.id);
    } finally {
      engine.setAgentExecutor(null);
    }
    return out;
  }

  it('itemized 任务 + 子任务还没全部 completed → coder-validator 判自己 success 但被引擎打回 fail', async () => {
    const companyId = 'co-gate-3';
    const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' });
    const parent = wdb.createTask({ goalId: goal.id, title: 'p', description: 'd', acceptanceMode: 'itemized' });
    const c1 = wdb.createTask({
      goalId: goal.id, title: 'c1', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
    });
    wdb.createTask({
      goalId: goal.id, title: 'c2', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
    });
    wdb.settleSubtaskResult(c1.id, 'completed'); // 只完成一半

    const out = await runValidator(companyId, parent.id);
    // 单节点图没有兜底边可退,fail 之后引擎会继续把它收敛成 blocked(等人)——
    // 这里要验的是「没被判 success」+「归因是 subtasks_incomplete」,不是具体停在哪个终态
    expect(out.node.status).not.toBe('success');
    expect(out.node.failReason).toBe('subtasks_incomplete');
  });

  it('itemized 任务 + 子任务全部 completed → coder-validator 正常判 success', async () => {
    const companyId = 'co-gate-4';
    const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' });
    const parent = wdb.createTask({ goalId: goal.id, title: 'p', description: 'd', acceptanceMode: 'itemized' });
    const c1 = wdb.createTask({
      goalId: goal.id, title: 'c1', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
    });
    const c2 = wdb.createTask({
      goalId: goal.id, title: 'c2', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
    });
    wdb.settleSubtaskResult(c1.id, 'completed');
    wdb.settleSubtaskResult(c2.id, 'completed');

    const out = await runValidator(companyId, parent.id);
    expect(out.node.status).toBe('success');
  });

  it('46 号文档 Phase 3:同一个 task 下还挂着 decomposed_work 子任务(未 completed)时,'
    + '不影响 acceptance_item 的验收判定——两套子任务完成判定互不干扰', async () => {
    const companyId = 'co-gate-6';
    const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' });
    const parent = wdb.createTask({ goalId: goal.id, title: 'p', description: 'd', acceptanceMode: 'itemized' });
    const c1 = wdb.createTask({
      goalId: goal.id, title: 'c1', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
    });
    wdb.settleSubtaskResult(c1.id, 'completed'); // 验收条目全部完成
    // decomposed_work 子任务还停在 draft,还没被 CEO enqueue、更不会 completed——
    // 如果闸门没收窄,这条会被当成"未完成的子任务"误伤,把上面本该通过的验收打成失败
    wdb.createTask({
      goalId: goal.id, title: 'todo-step', description: 'd', parentTaskId: parent.id,
      subtaskKind: 'decomposed_work',
      // 48 号文档:decomposed_work 创建时必须带验收契约(benchmark 段本身,不是 wrapper)
      acceptanceContract: { dimensions: [{ name: 'done', weight: 1, pass_threshold: 80 }] },
    });

    const out = await runValidator(companyId, parent.id);
    expect(out.node.status).toBe('success');
  });

  it('holistic 任务(没有子任务):闸门不生效,success 照常通过', async () => {
    const companyId = 'co-gate-5';
    const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' });
    const task = wdb.createTask({ goalId: goal.id, title: 't', description: 'd' }); // 缺省 holistic

    const out = await runValidator(companyId, task.id);
    expect(out.node.status).toBe('success');
  });
});