orggraph-planning-fixture.test.js
4.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* 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);
}
});
});