graph-engine-body-not-ready.test.js 8.3 KB
/**
 * Unit Tests — 35 号文档提案 A:本体完整性闸门接进图执行前置检查
 *
 * `agentLocator.missingBodyDefinition()` 此前只在招聘/建公司路径上被查过
 * (graph-service.js / graph-seed-service.js / company-service.js),图执行的
 * 核心路径(`runClaimedNode`)从未查过——本体因任何原因仍是空骨架时,引擎会
 * 原样把它当正常本体,派发一次真实调用(真实样本见 34 号文档 §五:`unit-tester`
 * 全程顶着 `_base` 骨架跑完了一整条真实任务,产出一份没有任何角色指导支撑的结果)。
 *
 * 本文件锁的是修完之后的行为:
 *   ① 本体不完整时,节点直接判 fail(`body_not_ready`),**不派发真实调用**
 *      (注入的 executor 断言从未被调用)
 *   ② 不扣预算(没有调用发生,「一次调用一定发生了」这条记账理由不成立)
 *   ③ 本体完整时行为不变(不动 §三十一/§三十三 号文档已经验过的正常路径)
 *
 * 隔离:monkeypatch `agent-locator.missingBodyDefinition`(模块级函数,`graph-engine.js`
 * 每次调用时都走 `locator.missingBodyDefinition(id)` 这个活引用,不是加载时快照,
 * patch 完之后立刻生效,不需要碰真实的 system/agents/ 目录),测试结束还原。
 */
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';

describe('graph-engine 本体完整性闸门(35 号文档提案 A)', () => {
  let wdb;
  let engine;
  let orgSvc;
  let locator;
  let runsDir;
  const TEST_DB_PATH = '/tmp/test-graph-engine-body-not-ready.sqlite';
  const origMissingBodyDefinition = () => {};

  beforeAll(() => {
    for (const f of [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]) {
      try { fs.unlinkSync(f); } catch {}
    }
    process.env.GRAPH_DB_PATH = TEST_DB_PATH;
    runsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graph-body-not-ready-runs-'));
    process.env.GRAPH_RUNS_DIR = runsDir;
    for (const m of [
      '../../services/graph-db',
      '../../services/graph-service',
      '../../services/org-graph-service',
      '../../services/work-graph-db',
      '../../services/graph-run-sse',
      '../../services/graph-engine',
      '../../services/agent-locator',
    ]) {
      delete require.cache[require.resolve(m)];
    }
    wdb = require('../../services/work-graph-db');
    engine = require('../../services/graph-engine');
    orgSvc = require('../../services/org-graph-service');
    locator = require('../../services/agent-locator');
  });

  afterAll(() => {
    for (const f of [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]) {
      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 ['../../services/graph-run-sse', '../../services/graph-engine']) {
      delete require.cache[require.resolve(m)];
    }
  });

  afterEach(() => {
    // 每条用例自己决定 patch 成什么,用完必须还原——同一个 locator 模块实例
    // 会被下一条用例复用(require.cache 只在 beforeAll 清过一次)
    locator.missingBodyDefinition = origMissingBodyDefinition;
    engine.setAgentExecutor(null);
  });

  function ensureAgent(id) {
    const db = wdb.getDb();
    const ts = 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}`, ts, ts);
  }

  function buildRun({ companyId, taskId, systemAgentId }) {
    ensureAgent(systemAgentId);
    const g = wdb.createWorkGraph({
      companyId, taskId,
      nodes: [{ nodeName: 'solo', systemAgentId }],
      edges: [],
    });
    expect(g.errors || []).toEqual([]);
    const run = wdb.createRun({
      graphKind: 'work', graphId: g.id, companyId, taskId,
      budget: { total_agent_calls: 10, total_tokens: 5_000_000, spent: { agent_calls: 0, tokens: 0 } },
    });
    engine.markRunReady(run.id);
    engine.startRun(run.id, { mode: 'manual' });
    return run.id;
  }

  // ①②:这道闸门只在**真会起 CLI 的执行器**下生效(与 `boundaryGuardApplicable`
  // 同一个判据,见 graph-engine.js 里那条注释)——所以这两条用例**不注入假 executor**,
  // 让 `agentExecutor` 保持模块默认值(`=== defaultAgentExecutor`),从而真正触发闸门;
  // 又因为闸门在派发之前就短路返回,`defaultAgentExecutor` 实际上从未被调用到,
  // 不会真的去起 Claude CLI 子进程。

  it('① 本体不完整:节点直接判 fail(body_not_ready),短路在真正派发之前', async () => {
    const companyId = 'co-body-not-ready';
    const taskId = 'task-body-not-ready';
    const runId = buildRun({ companyId, taskId, systemAgentId: 'zz-blank-body' });

    locator.missingBodyDefinition = (id) => (id === 'zz-blank-body' ? ['agent.md', 'soul.md'] : []);

    const out = await engine.advanceOne(runId);
    expect(out.ok).toBe(true);

    const state = wdb.getRunState(runId);
    expect(state.nodes[0].status).toBe('blocked');
    expect(state.nodes[0].failReason).toBe('body_not_ready');
  });

  it('② 本体不完整时不扣预算——没有调用发生,「一次调用一定发生了」这条记账理由不成立', async () => {
    const companyId = 'co-body-not-ready-budget';
    const taskId = 'task-body-not-ready-budget';
    const runId = buildRun({ companyId, taskId, systemAgentId: 'zz-blank-body-2' });

    locator.missingBodyDefinition = () => ['agent.md'];

    await engine.advanceOne(runId);

    const run = wdb.getRun(runId);
    const spent = JSON.parse(run.budget_json).spent;
    expect(spent.agent_calls).toBe(0);
  });

  it('③ 注入假 executor 时这道闸门不生效——保护全仓库其余用「假 systemAgentId + 假 executor」的用例', async () => {
    const companyId = 'co-body-ready';
    const taskId = 'task-body-ready';
    const runId = buildRun({ companyId, taskId, systemAgentId: 'zz-fixture-only-agent' });

    // 就算把它判成"缺失",只要走的是注入的假 executor,闸门也不该拦——这正是本改动
    // 第一版把全仓库测试炸穿的那个坑(真实文件系统里几乎没有一个测试夹具用的
    // systemAgentId 真的对应 system/agents/ 下的目录),修完之后要把它钉住不再复现。
    locator.missingBodyDefinition = () => ['agent.md'];

    let executorCalled = false;
    engine.setAgentExecutor(async () => {
      executorCalled = true;
      return { status: 'success' };
    });

    await engine.advanceOne(runId);
    expect(executorCalled).toBe(true);

    const run = wdb.getRun(runId);
    expect(JSON.parse(run.budget_json).spent.agent_calls).toBe(1);

    const state = wdb.getRunState(runId);
    expect(state.nodes[0].status).toBe('success');
  });

  it('④ subgraph 节点不受这道闸门约束——它派发的是 workgraph-planning 规划子图,不是本体自己', async () => {
    const companyId = 'tmp-body-subgraph';
    const taskId = 'task-body-subgraph';
    ensureAgent('zz-org-node-agent');
    expect(orgSvc.writeGraph(companyId, {
      nodes: [{ roleName: 'org-solo', systemAgentId: 'zz-org-node-agent' }],
      edges: [],
    }).ok).toBe(true);

    const run = wdb.createRun({
      graphKind: 'org', graphId: companyId, companyId, taskId,
      budget: { total_agent_calls: 10, total_tokens: 5_000_000, spent: { agent_calls: 0, tokens: 0 } },
    });
    engine.markRunReady(run.id);
    engine.startRun(run.id, { mode: 'manual' });

    // 就算这个"本体"是空骨架,subgraph 节点也不该被这道闸门拦——它不直接调用这个本体,
    // 它派发的是 workgraph-planning 去规划子图
    locator.missingBodyDefinition = () => ['agent.md'];

    let planCalled = false;
    engine.setPlanWorkGraph(async () => {
      planCalled = true;
      return { draft: { nodes: [{ nodeName: 'x', systemAgentId: 'zz-org-node-agent' }], edges: [] }, source: 'test' };
    });

    try {
      await engine.advanceOne(run.id);
      expect(planCalled).toBe(true);
    } finally {
      engine.setPlanWorkGraph(null);
    }
  });
});