/** * Unit Tests — fixtures/devloop-mock/orggraph-planning.json(P3B / W8) * * 这份 fixture 的价值全在「能不能被读回并落库」,所以本文件按四环各钉一颗钉子: * 产生 → chunks 里确实有一段 ```json 图(不是只写在 result 里,那样 agent-chat 的 * fullOutput 收不到,见 agent-chat.js:2315 只累积 agent_message_chunk) * 传递 → mock 协议按 agent id(含 `{cid}:` 前缀的作用域名)能命中这份 fixture 并把 * 图原样吐进流式 chunks * 落库 → 这张图过得了 org-graph-service 的五项校验(真正的写入方 POST /org-graph 用的就是它) * 读回 → 形状与 system/org-graph-templates/dev.json 对齐:三节点 + 两条 fail 回退边 * * 反例保护:把图从 chunks 挪到 result、或让回退边漏标 backward、或砍掉那条兜底 fail 边, * 本文件都会红。 */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import fs from 'fs'; import path from 'path'; const REPO = path.resolve(__dirname, '../../..'); const FIXTURE = path.join(REPO, 'fixtures/devloop-mock/orggraph-planning.json'); /** ```json 围栏块(与 graph-engine.extractPlannedGraph 同一取法) */ function fencedGraphs(text) { const out = []; const fence = /```(?:json|jsonc)?\s*([\s\S]*?)```/g; let m; while ((m = fence.exec(text))) { try { const doc = JSON.parse(m[1]); if (Array.isArray(doc.nodes)) out.push(doc); } catch { /* 非图的围栏块,跳过 */ } } return out; } describe('orggraph-planning mock fixture', () => { const fixture = JSON.parse(fs.readFileSync(FIXTURE, 'utf-8')); const chunkText = (fixture.chunks || []).join(''); it('图放在 chunks 里(而不是只放 result)', () => { expect(fencedGraphs(chunkText)).toHaveLength(1); // result 只是收尾摘要,不该承载图 —— 它进不了 agent-chat 的 fullOutput expect(fencedGraphs(String(fixture.result || ''))).toHaveLength(0); }); it('形状对齐 system/org-graph-templates/dev.json:三节点 + 两条 fail 回退边', () => { const g = fencedGraphs(chunkText)[0]; const dev = JSON.parse(fs.readFileSync(path.join(REPO, 'system/org-graph-templates/dev.json'), 'utf-8')); expect(g.nodes.map((n) => n.roleName)).toEqual(dev.nodes); expect(g.edges.filter((e) => e.condition === 'success')).toHaveLength(2); const fails = g.edges.filter((e) => e.condition === 'fail'); expect(fails).toHaveLength(2); expect(fails.every((e) => e.edgeKind === 'backward')).toBe(true); // 兜底边:有 fail 出边的节点必须留一条不带 failReason 的(org-graph-service 的 fail-fallback-edge 规则) expect(fails.filter((e) => !e.failReason)).toHaveLength(1); }); /** * validateGraph 是纯函数(不碰 DB,graph-db 的连接只在 getDb() 里惰性打开), * 所以这里既不设 GRAPH_DB_PATH 也不清 require.cache —— 那套隔离手法会污染同一 * worker 里后跑的 graph-seed-service.test.js(实测:加进去后它必红)。 */ it('这张图过得了 org-graph-service 的五项校验(真正的写入方用的就是它)', () => { const svc = require('../../services/org-graph-service'); const g = fencedGraphs(chunkText)[0]; expect(svc.validateGraph(g.nodes, g.edges).errors).toEqual([]); }); /** * fixture 的查找键是 `agentInfo.name` 冒号后那段(ClaudeCliProtocol._sendMockPrompt)。 * graph-engine 传的是 `{cid}:{agentId}`,命得中;agent-chat 传的是 identity.md 的显示名, * 命不中(W8 备注里记了这个断点)。这条用例钉住的是「按 agent id 传就必须命中」这半边契约。 */ it('mock 协议按 agent id / 公司作用域名都能回放出这张图', () => { const { ClaudeCliProtocol } = require('../../../packages/acp-core/protocol/ClaudeCliProtocol'); for (const name of ['orggraph-planning', 'some-co:orggraph-planning']) { const p = new ClaudeCliProtocol({ name }, { mock: true }); let text = ''; p.on('session_update', (e) => { text += e.update.content.text; }); p._sendMockPrompt('规划这家公司的 org graph'); expect(fencedGraphs(text)).toHaveLength(1); } }); });