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