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