graph-engine-node-outcome-memory.test.js 8.01 KB
/**
 * Unit Tests — 图执行结果桥接进 memory.md(36 号文档:Innovator/Dream 训练闭环可行性调研)
 *
 * 36 号文档实查发现的头号阻塞项:`graph-engine.js` 从不写 `memory.md`(唯一写入路径是
 * 人类对话 `agent-chat.js`),导致 Dream 每日扫描时,org/work graph 里真正干活的专业节点
 * (`prd-planning`/`coder`/`benchmark-validator`……)的 memory.md 永远是模板空壳——不是
 * "原料脏",是压根没有原料。本文件锁的是补上的桥接点(`runClaimedNode` 结算之后调用
 * `recordNodeOutcomeMemory`):
 *   ① 真实派发 + 本体已雇佣(bodyDir 存在)+ fail(body_not_ready) → 写一行摘要
 *      (借道 body_not_ready 端到端验证整条 isRealDispatch 真实链路——它跟其它"真派发"
 *      场景一样满足 agentExecutor===defaultAgentExecutor,却在真正调用 CLI 之前短路,
 *      是全仓库唯一能不起真 CLI 就摸到 isRealDispatch===true 的路径,35 号文档同款手法)
 *   ② 本体已雇佣,但 bodyDir 不存在 → 不写,避免测试夹具/未雇佣本体把假地址灌进
 *      真实 system/agents/ 目录(与 missingBody 同一条判据,多一层防线)
 *   ③ 注入假 executor(全仓库其余用例的常态)→ 不写,不干扰任何现有测试
 *   ④ `recordNodeOutcomeMemory` 直调:success 一行的格式化(advanceOne 端到端摸不到
 *      这条分支——isRealDispatch 为真时 else 分支会走真实 defaultAgentExecutor,起真 CLI,
 *      不适合单测;直调覆盖纯格式化逻辑,属于"最小可运行检查")
 *
 * 隔离手法同 graph-engine-body-not-ready.test.js:monkeypatch 模块级活引用
 * (`locator.missingBodyDefinition` / `locator.bodyDir` / `memoryService.appendToMemoryMd`),
 * 不碰真实 system/agents/ 目录、不做真实文件 I/O。
 */
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';

describe('图执行结果桥接进 memory.md(36 号文档)', () => {
  let wdb;
  let engine;
  let locator;
  let memoryService;
  let runsDir;
  const TEST_DB_PATH = '/tmp/test-graph-engine-node-outcome-memory.sqlite';
  let origBodyDir;
  let origAppendToMemoryMd;
  let calls;

  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-node-outcome-memory-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',
      '../../services/memory-service',
    ]) {
      delete require.cache[require.resolve(m)];
    }
    wdb = require('../../services/work-graph-db');
    engine = require('../../services/graph-engine');
    locator = require('../../services/agent-locator');
    memoryService = require('../../services/memory-service');
    origBodyDir = locator.bodyDir;
    origAppendToMemoryMd = memoryService.appendToMemoryMd;
  });

  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(() => {
    locator.missingBodyDefinition = () => [];
    locator.bodyDir = origBodyDir;
    memoryService.appendToMemoryMd = origAppendToMemoryMd;
    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, nodeName = 'solo' }) {
    ensureAgent(systemAgentId);
    const g = wdb.createWorkGraph({
      companyId, taskId,
      nodes: [{ nodeName, 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;
  }

  function spyMemory() {
    calls = [];
    memoryService.appendToMemoryMd = (...args) => { calls.push(args); };
  }

  it('① 真实派发(借道 body_not_ready 短路,不起真 CLI)+ bodyDir 存在 → 记一行摘要', async () => {
    spyMemory();
    const existingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zz-hired-body-'));
    locator.bodyDir = (id) => (id === 'zz-hired-agent-1' ? existingDir : origBodyDir(id));
    // 本体判为不完整——不会真的调用 defaultAgentExecutor(body_not_ready 在派发之前短路),
    // 但仍然是 isRealDispatch===true 的真实结算路径,settled.node.failReason 是真结算出来的。
    locator.missingBodyDefinition = (id) => (id === 'zz-hired-agent-1' ? ['agent.md'] : []);

    const runId = buildRun({ companyId: 'co-mem-1', taskId: 'task-mem-1', systemAgentId: 'zz-hired-agent-1', nodeName: '实现节点' });
    const out = await engine.advanceOne(runId);
    expect(out.ok).toBe(true);

    expect(calls.length).toBe(1);
    const [addr, task, result] = calls[0];
    expect(addr).toBe('co-mem-1:zz-hired-agent-1');
    expect(task).toContain('实现节点');
    expect(task).toContain('第 1 次运行');
    expect(result).toBe('fail: body_not_ready');

    fs.rmSync(existingDir, { recursive: true, force: true });
  });

  it('② bodyDir 不存在(未雇佣/测试夹具)→ 不记,避免灌脏真实 system/agents/', async () => {
    spyMemory();
    locator.bodyDir = (id) => (id === 'zz-unhired-agent' ? path.join(os.tmpdir(), 'zz-graph-engine-memory-test-does-not-exist') : origBodyDir(id));
    locator.missingBodyDefinition = (id) => (id === 'zz-unhired-agent' ? ['agent.md'] : []);

    const runId = buildRun({ companyId: 'co-mem-2', taskId: 'task-mem-2', systemAgentId: 'zz-unhired-agent' });
    await engine.advanceOne(runId);

    expect(calls.length).toBe(0);
  });

  it('③ 注入假 executor(isRealDispatch=false)→ 不记,不干扰全仓库其余用例', async () => {
    spyMemory();
    const existingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zz-hired-body-fake-exec-'));
    locator.bodyDir = (id) => (id === 'zz-fake-exec-agent' ? existingDir : origBodyDir(id));
    locator.missingBodyDefinition = () => ['agent.md']; // 就算判成缺失也不该拦——闸门本身就不该生效

    engine.setAgentExecutor(async () => ({ status: 'success' }));
    const runId = buildRun({ companyId: 'co-mem-3', taskId: 'task-mem-3', systemAgentId: 'zz-fake-exec-agent' });
    await engine.advanceOne(runId);

    expect(calls.length).toBe(0);

    fs.rmSync(existingDir, { recursive: true, force: true });
  });

  it('④ recordNodeOutcomeMemory 直调:success 一行格式化正确', () => {
    spyMemory();
    engine.recordNodeOutcomeMemory(
      { companyId: 'co-mem-4', systemAgentId: 'zz-direct-agent', nodeName: '校验节点', attempt: 2 },
      { status: 'success', failReason: null },
    );

    expect(calls.length).toBe(1);
    const [addr, task, result] = calls[0];
    expect(addr).toBe('co-mem-4:zz-direct-agent');
    expect(task).toContain('校验节点');
    expect(task).toContain('第 3 次运行');
    expect(result).toBe('success');
  });
});