graph-engine-workgraph-planning-memory.test.js 6.59 KB
/**
 * Unit Tests — subgraph 节点结算 → workgraph-planning 自己的 memory.md 桥接(36 号文档追加)
 *
 * 背景:36 号文档真实回归发现 `workgraph-planning` 的图形状裁剪判据现在只有"允许裁剪"
 * 没有"具体判据"(单页静态网站任务原样照抄 planner.json 全部 5 节点,一个都没砍),而它
 * 自己完全没有可积累的历史——阶段一的 `recordNodeOutcomeMemory` 桥接明确排除了 subgraph
 * 节点(那是给 agent_call 节点记的,subgraph 节点没有对应的"真实执行"概念)。
 *
 * 本文件锁的是补上的那半:subgraph 节点结算(= 它规划出的子 work graph 最终收敛)时,
 * 把"这次规划的图跑得怎么样"记进 `workgraph-planning` 自己的 memory.md(不是宿主 org node
 * 的 `ctx.systemAgentId`,那是宿主自己的身份,不是真正执行规划的 agent——规划本体全局唯一)。
 *
 *   ① 真实规划(`planWorkGraph === defaultPlanWorkGraph`)+ success → 记 "success"
 *   ② 真实规划 + fail(reason) → 记 "fail: <reason>"
 *   ③ 注入假 planWorkGraph(全仓库现有 subgraph 测试的常态,见 graph-engine-revive-parent.test.js
 *      的 buildParentWithFailingChild)→ 不记,不能因为这次改动就把假地址写进真实
 *      system/agents/workgraph-planning/ 目录
 *
 * ①②借道直调(不必真起 CLI 触发 planWorkGraph===defaultPlanWorkGraph 才能验证格式化),
 * ③走完整的 subgraph 结算流程(复用 graph-engine-revive-parent.test.js 的建图手法),
 * 确认现有"全仓库 subgraph 测试都注入假 planWorkGraph"这条约定下这个新桥接确实按兵不动。
 */
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';

describe('subgraph 节点结算桥接进 workgraph-planning 的 memory.md(36 号文档)', () => {
  let wdb;
  let engine;
  let memoryService;
  let runsDir;
  const TEST_DB_PATH = '/tmp/test-graph-engine-workgraph-planning-memory.sqlite';
  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-wgplan-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/memory-service',
    ]) {
      delete require.cache[require.resolve(m)];
    }
    wdb = require('../../services/work-graph-db');
    engine = require('../../services/graph-engine');
    memoryService = require('../../services/memory-service');
    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;
  });

  beforeEach(() => {
    const db = wdb.getDb();
    db.pragma('foreign_keys = OFF');
    db.exec(`
      DELETE FROM graph_run_loop_records;
      DELETE FROM graph_run_nodes;
      DELETE FROM graph_runs;
      DELETE FROM work_graph_edges;
      DELETE FROM work_graph_nodes;
      DELETE FROM work_graphs;
      DELETE FROM system_agents;
    `);
    db.pragma('foreign_keys = ON');
    engine.setAgentExecutor(null);
    engine.setPlanWorkGraph(null);
    memoryService.appendToMemoryMd = origAppendToMemoryMd;
  });

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

  it('① 真实规划(直调,不必起真 CLI)+ success → 记一行 "success"', () => {
    spyMemory();
    engine.recordWorkgraphPlanningMemory('co-wgplan-1', 'org-coder', 'success', null);

    expect(calls.length).toBe(1);
    const [addr, task, result] = calls[0];
    expect(addr).toBe('co-wgplan-1:workgraph-planning');
    expect(task).toContain('org-coder');
    expect(result).toBe('success');
  });

  it('② 真实规划(直调)+ fail(reason) → 记一行 "fail: <reason>"', () => {
    spyMemory();
    engine.recordWorkgraphPlanningMemory('co-wgplan-2', 'org-validator', 'fail', 'impl_bug');

    expect(calls.length).toBe(1);
    const [addr, , result] = calls[0];
    expect(addr).toBe('co-wgplan-2:workgraph-planning');
    expect(result).toBe('fail: impl_bug');
  });

  // ③:复用 graph-engine-revive-parent.test.js 的建图手法——父 run 一个 subgraph 节点
  // (宿主 org node)+ 一张会失败/成功的子图,全程注入假 setPlanWorkGraph(全仓库 subgraph
  // 测试的常态:defaultPlanWorkGraph 会真的起 CLI 调 workgraph-planning)。
  function ensureAgents(ids) {
    const db = wdb.getDb();
    const now = new Date().toISOString();
    const ins = 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', ?, ?)
    `);
    for (const id of new Set(ids)) ins.run(id, id, `system/agents/${id}`, now, now);
  }

  it('③ 注入假 planWorkGraph(全仓库现有 subgraph 测试的常态)→ 不记,不污染真实目录', async () => {
    spyMemory();
    const companyId = 'co-wgplan-3';
    ensureAgents(['org-coder', 'wg-only']);

    const childG = wdb.createWorkGraph({ companyId, nodes: ['wg-only'], edges: [] });
    expect(childG.errors || []).toEqual([]);

    const parentG = wdb.createWorkGraph({
      companyId,
      nodes: [{ nodeName: 'org-coder', systemAgentId: 'org-coder', nodeKind: 'subgraph' }],
      edges: [],
    });
    expect(parentG.errors || []).toEqual([]);

    const parentRun = wdb.createRun({ graphKind: 'work', graphId: parentG.id, companyId, executionMode: 'manual' });
    engine.markRunReady(parentRun.id);
    engine.startRun(parentRun.id, { mode: 'manual' });

    engine.setPlanWorkGraph(() => ({ workGraphId: childG.id }));
    engine.setAgentExecutor(async () => ({ status: 'success', output: '子节点成功' }));

    for (let i = 0; i < 12; i += 1) {
      const r = await engine.advanceTree(parentRun.id);
      if (!r || r.ok === false) break;
    }

    expect(wdb.getRunState(parentRun.id).run.run_status).toBe('completed');
    expect(calls.length).toBe(0);
  });
});