@zhushanwen/pi-session-reader 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/execution-tree.test.ts +792 -0
- package/src/__tests__/family.test.ts +150 -0
- package/src/__tests__/find.test.ts +342 -1
- package/src/__tests__/index.test.ts +50 -0
- package/src/__tests__/subagents.test.ts +291 -14
- package/src/__tests__/tool-handler.test.ts +862 -3
- package/src/__tests__/tree.test.ts +20 -0
- package/src/__tests__/workflow.test.ts +282 -0
- package/src/core/execution-tree.ts +517 -0
- package/src/core/family.ts +40 -0
- package/src/core/tree.ts +14 -5
- package/src/core/workflow.ts +329 -0
- package/src/discovery/find.ts +167 -20
- package/src/discovery/subagents.ts +113 -152
- package/src/discovery/workflows.ts +162 -0
- package/src/index.ts +22 -4
- package/src/tool-handler.ts +334 -21
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
4
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { buildExecutionTree, formatExecutionTreeText, type ExecutionTreeNode } from '../core/execution-tree.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* M3b U7 buildExecutionTree 单测(design m3b 6 testCases)。
|
|
10
|
+
*
|
|
11
|
+
* 测试框架 vitest(禁止 node:test/tsx)。fixture 自包含(mkdtemp 临时目录),复用
|
|
12
|
+
* subagents.test.ts 验证过的 fixture 拓扑(main session + subagent session + records manifest
|
|
13
|
+
* + workflow-state 文件 + workflow-state-link)。
|
|
14
|
+
*
|
|
15
|
+
* 覆盖:
|
|
16
|
+
* - TC-m3b-nested-tree:任意深度 subagent↔workflow-call 相互嵌套(parentRecordId 精确链)
|
|
17
|
+
* - TC-m3b-flat-fallback:旧机制扁平回退(全无 parentRecordId)
|
|
18
|
+
* - TC-m3b-cycle-detection:workflow 指针环(A→B→A),visited Set 防环
|
|
19
|
+
* - TC-m3b-single-node:单节点树(无后代,ES5)
|
|
20
|
+
* - TC-m3b-source-priority:parentRecordId 三级数据源优先级(manifest>identity>flat,DM4)
|
|
21
|
+
* - TC-m3b-real-data-guard:本机真实数据 flat 回退(旧机制,skipIf CI 无数据)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ---- fixture 常量(uuid 特征,互不为子串,满足 extractSessionIdFromFilename)----
|
|
25
|
+
const MAIN = '0aaaaaaa-bbbb-7ccc-dddd-000000000001'
|
|
26
|
+
const A_REAL = '0aaaaaaa-bbbb-7ccc-dddd-000000000002'
|
|
27
|
+
const B_REAL = '0aaaaaaa-bbbb-7ccc-dddd-000000000003'
|
|
28
|
+
const C_REAL = '0aaaaaaa-bbbb-7ccc-dddd-000000000004'
|
|
29
|
+
const D_REAL = '0aaaaaaa-bbbb-7ccc-dddd-000000000005'
|
|
30
|
+
const X_REAL = '0aaaaaaa-bbbb-7ccc-dddd-000000000006'
|
|
31
|
+
// workflow-state-link runId
|
|
32
|
+
const RUN_A = 'wf-1786121304924-runA'
|
|
33
|
+
const RUN_B = 'wf-1786121304924-runB'
|
|
34
|
+
|
|
35
|
+
/** 真实 pi agent 目录(本机),用于集成测试守卫。 */
|
|
36
|
+
const REAL_AGENT_DIR = '/Users/zhushanwen/.pi/agent'
|
|
37
|
+
const HAS_REAL = (() => {
|
|
38
|
+
try {
|
|
39
|
+
return (
|
|
40
|
+
execSync(`find ${REAL_AGENT_DIR}/sessions -maxdepth 1 -type d 2>/dev/null | head -1`, {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
}).trim().length > 0
|
|
43
|
+
)
|
|
44
|
+
} catch {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
})()
|
|
48
|
+
|
|
49
|
+
// ---- fixture helpers(自包含,拓扑同 subagents.test.ts)----
|
|
50
|
+
|
|
51
|
+
async function makeAgentDir(): Promise<string> {
|
|
52
|
+
return mkdtemp(join(tmpdir(), 'exec-tree-test-'))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 写 main session 文件(首行 header)。 */
|
|
56
|
+
async function writeMainSession(dir: string, slug: string, id: string): Promise<string> {
|
|
57
|
+
const sessionDir = join(dir, 'sessions', slug)
|
|
58
|
+
await mkdir(sessionDir, { recursive: true })
|
|
59
|
+
const path = join(sessionDir, `${id}.jsonl`)
|
|
60
|
+
await writeFile(path, JSON.stringify({ type: 'session', id, cwd: `/proj/${slug}` }) + '\n')
|
|
61
|
+
return path
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 写 subagent session 文件:首行 header(真实 id)+ 占位 message + 尾行 identity。
|
|
66
|
+
* identity 尾行含 rootSessionId/slug/agent,可选 parentRecordId(数据源 ② 测试用)。
|
|
67
|
+
*/
|
|
68
|
+
async function writeSubagentSession(
|
|
69
|
+
dir: string,
|
|
70
|
+
slug: string,
|
|
71
|
+
realId: string,
|
|
72
|
+
identity: {
|
|
73
|
+
rootSessionId: string
|
|
74
|
+
slug: string
|
|
75
|
+
agent?: string
|
|
76
|
+
parentRecordId?: string
|
|
77
|
+
task?: string
|
|
78
|
+
},
|
|
79
|
+
): Promise<string> {
|
|
80
|
+
const sessionDir = join(dir, 'subagents', slug, 'sessions')
|
|
81
|
+
await mkdir(sessionDir, { recursive: true })
|
|
82
|
+
const path = join(sessionDir, `${realId}.jsonl`)
|
|
83
|
+
const data: Record<string, unknown> = {
|
|
84
|
+
id: `sa-${realId.slice(0, 8)}`,
|
|
85
|
+
rootSessionId: identity.rootSessionId,
|
|
86
|
+
slug: identity.slug,
|
|
87
|
+
agent: identity.agent ?? 'explorer',
|
|
88
|
+
mode: 'sync',
|
|
89
|
+
task: identity.task ?? 'do work',
|
|
90
|
+
startedAt: 1,
|
|
91
|
+
}
|
|
92
|
+
if (identity.parentRecordId !== undefined) data.parentRecordId = identity.parentRecordId
|
|
93
|
+
const lines = [
|
|
94
|
+
JSON.stringify({ type: 'session', id: realId, cwd: `/proj/${slug}` }),
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
type: 'message',
|
|
97
|
+
id: 'm1',
|
|
98
|
+
parentId: realId,
|
|
99
|
+
message: { role: 'user', content: 'do work' },
|
|
100
|
+
}),
|
|
101
|
+
JSON.stringify({ type: 'custom', customType: 'subagent-identity', data }),
|
|
102
|
+
]
|
|
103
|
+
await writeFile(path, lines.join('\n') + '\n')
|
|
104
|
+
return path
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 写 records manifest。fields 含必填 + 可选 parentRecordId(数据源 ①)。 */
|
|
108
|
+
async function writeRecordManifest(
|
|
109
|
+
dir: string,
|
|
110
|
+
slug: string,
|
|
111
|
+
id: string,
|
|
112
|
+
fields: {
|
|
113
|
+
rootSessionId: string
|
|
114
|
+
agentName?: string
|
|
115
|
+
sessionFile: string
|
|
116
|
+
task?: string
|
|
117
|
+
slug?: string
|
|
118
|
+
model?: string
|
|
119
|
+
status?: string
|
|
120
|
+
parentRecordId?: string
|
|
121
|
+
},
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
const recordsDir = join(dir, 'subagents', slug, 'records')
|
|
124
|
+
await mkdir(recordsDir, { recursive: true })
|
|
125
|
+
await writeFile(join(recordsDir, `${id}.json`), JSON.stringify({ id, ...fields }))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 写 wf-state 文件(NEW 格式 v=wf-run-v1,calls[].sessionFile)。返回绝对路径。 */
|
|
129
|
+
async function writeWfState(
|
|
130
|
+
dir: string,
|
|
131
|
+
slug: string,
|
|
132
|
+
runId: string,
|
|
133
|
+
callSessionFiles: string[],
|
|
134
|
+
): Promise<string> {
|
|
135
|
+
const wfDir = join(dir, 'sessions', slug, 'workflow-state')
|
|
136
|
+
await mkdir(wfDir, { recursive: true })
|
|
137
|
+
const path = join(wfDir, `${runId}.jsonl`)
|
|
138
|
+
const snap = JSON.stringify({
|
|
139
|
+
v: 'wf-run-v1',
|
|
140
|
+
runId,
|
|
141
|
+
state: {
|
|
142
|
+
status: 'done',
|
|
143
|
+
calls: callSessionFiles.map((sf, i) => ({ id: i, status: 'done', sessionFile: sf })),
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
await writeFile(path, snap + '\n')
|
|
147
|
+
return path
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 向 session 文件追加 workflow-state-link custom entry。 */
|
|
151
|
+
async function writeWfLink(
|
|
152
|
+
sessionPath: string,
|
|
153
|
+
sessionId: string,
|
|
154
|
+
link: { runId: string; path: string },
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
const line = JSON.stringify({
|
|
157
|
+
type: 'custom',
|
|
158
|
+
id: `wf-link-${link.runId}`,
|
|
159
|
+
parentId: sessionId,
|
|
160
|
+
customType: 'workflow-state-link',
|
|
161
|
+
data: { runId: link.runId, path: link.path, updatedAt: '2026-08-07T16:48:24.933Z' },
|
|
162
|
+
})
|
|
163
|
+
await writeFile(sessionPath, line + '\n', { flag: 'a' })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 在节点树里按 type+sessionId 查找节点(DFS)。 */
|
|
167
|
+
function findNode(
|
|
168
|
+
node: ExecutionTreeNode,
|
|
169
|
+
type: string,
|
|
170
|
+
sidPrefix: string,
|
|
171
|
+
): ExecutionTreeNode | undefined {
|
|
172
|
+
if (node.type === type && node.sessionId.startsWith(sidPrefix)) return node
|
|
173
|
+
for (const c of node.children) {
|
|
174
|
+
const found = findNode(c, type, sidPrefix)
|
|
175
|
+
if (found) return found
|
|
176
|
+
}
|
|
177
|
+
return undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================
|
|
181
|
+
// fixture 用例
|
|
182
|
+
// ============================================================
|
|
183
|
+
|
|
184
|
+
describe('buildExecutionTree - fixture', () => {
|
|
185
|
+
let dir: string
|
|
186
|
+
|
|
187
|
+
beforeEach(async () => {
|
|
188
|
+
dir = await makeAgentDir()
|
|
189
|
+
})
|
|
190
|
+
afterEach(async () => {
|
|
191
|
+
await rm(dir, { recursive: true, force: true })
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('TC-m3b-nested-tree:main→subagent A→{workflow-call B, subagent C} 相互嵌套(parentRecordId 精确链)', async () => {
|
|
195
|
+
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
196
|
+
// subagent A(顶层,parentRecordId=undefined),sessionFile 含 workflow-state-link → call session B
|
|
197
|
+
const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
|
|
198
|
+
rootSessionId: MAIN,
|
|
199
|
+
slug: 'sub-a',
|
|
200
|
+
})
|
|
201
|
+
const recAId = 'sa-record-A'
|
|
202
|
+
await writeRecordManifest(dir, '--main-cwd--', recAId, {
|
|
203
|
+
rootSessionId: MAIN,
|
|
204
|
+
agentName: 'explorer',
|
|
205
|
+
sessionFile: saA,
|
|
206
|
+
status: 'completed',
|
|
207
|
+
parentRecordId: undefined,
|
|
208
|
+
})
|
|
209
|
+
// subagent C(A 的后代,parentRecordId=recAId,精确链)
|
|
210
|
+
const saC = await writeSubagentSession(dir, '--main-cwd--', C_REAL, {
|
|
211
|
+
rootSessionId: MAIN,
|
|
212
|
+
slug: 'sub-c',
|
|
213
|
+
})
|
|
214
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-record-C', {
|
|
215
|
+
rootSessionId: MAIN,
|
|
216
|
+
agentName: 'worker',
|
|
217
|
+
sessionFile: saC,
|
|
218
|
+
status: 'completed',
|
|
219
|
+
parentRecordId: recAId,
|
|
220
|
+
})
|
|
221
|
+
// workflow-call B:A 的 sessionFile 的 workflow-state-link → wf-state → calls[].sessionFile = SB
|
|
222
|
+
const sbB = join(
|
|
223
|
+
dir,
|
|
224
|
+
'subagents',
|
|
225
|
+
'--main-cwd--',
|
|
226
|
+
'sessions',
|
|
227
|
+
`2026-08-07T16-49-48-393Z_${B_REAL}.jsonl`,
|
|
228
|
+
)
|
|
229
|
+
// B call session 文件需存在(resolveWorkflows 读 call sessionFile;这里只需路径,不读内容建树)
|
|
230
|
+
await mkdir(join(dir, 'subagents', '--main-cwd--', 'sessions'), { recursive: true })
|
|
231
|
+
await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
|
|
232
|
+
const wfPath = await writeWfState(dir, '--main-cwd--', RUN_A, [sbB])
|
|
233
|
+
await writeWfLink(saA, A_REAL, { runId: RUN_A, path: wfPath })
|
|
234
|
+
|
|
235
|
+
const tree = await buildExecutionTree(MAIN, dir)
|
|
236
|
+
|
|
237
|
+
// 根节点
|
|
238
|
+
expect(tree.root.type).toBe('main')
|
|
239
|
+
expect(tree.root.sessionId).toBe(MAIN)
|
|
240
|
+
// root.children 含 A(subagent)
|
|
241
|
+
const nodeA = findNode(tree.root, 'subagent', A_REAL)
|
|
242
|
+
expect(nodeA).toBeDefined()
|
|
243
|
+
expect(tree.root.children.some((c) => c === nodeA)).toBe(true)
|
|
244
|
+
// A.children 含 B(workflow-call,runId/stateFile)
|
|
245
|
+
const nodeB = findNode(tree.root, 'workflow-call', B_REAL)
|
|
246
|
+
expect(nodeB).toBeDefined()
|
|
247
|
+
expect(nodeA!.children.some((c) => c === nodeB)).toBe(true)
|
|
248
|
+
expect(nodeB!.runId).toBe(RUN_A)
|
|
249
|
+
// A.children 含 C(subagent,parentRecordId=recAId,严格 parentRecordId 链挂 A 下)
|
|
250
|
+
const nodeC = findNode(tree.root, 'subagent', C_REAL)
|
|
251
|
+
expect(nodeC).toBeDefined()
|
|
252
|
+
expect(nodeA!.children.some((c) => c === nodeC)).toBe(true)
|
|
253
|
+
expect(nodeC!.parentRecordId).toBe(recAId)
|
|
254
|
+
// 树规模与精度
|
|
255
|
+
expect(tree.totalNodes).toBe(4) // main + A + B + C
|
|
256
|
+
expect(tree.maxDepth).toBe(2) // root=0, A=1, B/C=2
|
|
257
|
+
expect(tree.sourceMode).toBe('precise')
|
|
258
|
+
expect(tree.truncated).toBe(false)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('TC-m3b-flat-fallback:旧机制全无 parentRecordId → 全挂 main(扁平)+ flat-fallback', async () => {
|
|
262
|
+
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
263
|
+
// 3 个顶层 subagent,全无 parentRecordId(旧机制)
|
|
264
|
+
const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
|
|
265
|
+
rootSessionId: MAIN,
|
|
266
|
+
slug: 'a',
|
|
267
|
+
})
|
|
268
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-A', {
|
|
269
|
+
rootSessionId: MAIN,
|
|
270
|
+
agentName: 'explorer',
|
|
271
|
+
sessionFile: saA,
|
|
272
|
+
})
|
|
273
|
+
const saB = await writeSubagentSession(dir, '--main-cwd--', B_REAL, {
|
|
274
|
+
rootSessionId: MAIN,
|
|
275
|
+
slug: 'b',
|
|
276
|
+
})
|
|
277
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-B', {
|
|
278
|
+
rootSessionId: MAIN,
|
|
279
|
+
agentName: 'explorer',
|
|
280
|
+
sessionFile: saB,
|
|
281
|
+
})
|
|
282
|
+
const saC = await writeSubagentSession(dir, '--main-cwd--', C_REAL, {
|
|
283
|
+
rootSessionId: MAIN,
|
|
284
|
+
slug: 'c',
|
|
285
|
+
})
|
|
286
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-C', {
|
|
287
|
+
rootSessionId: MAIN,
|
|
288
|
+
agentName: 'explorer',
|
|
289
|
+
sessionFile: saC,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const tree = await buildExecutionTree(MAIN, dir)
|
|
293
|
+
|
|
294
|
+
// 全挂 main(flat)
|
|
295
|
+
expect(tree.root.children).toHaveLength(3)
|
|
296
|
+
expect(tree.root.children.every((c) => c.type === 'subagent')).toBe(true)
|
|
297
|
+
// 不丢弃任何 record
|
|
298
|
+
expect(tree.totalNodes).toBe(4) // main + 3 subagent
|
|
299
|
+
expect(tree.sourceMode).toBe('flat-fallback')
|
|
300
|
+
expect(tree.truncated).toBe(false)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('TC-m3b-cycle-detection:workflow 指针环 A→B→A,visited Set 防环 + truncated', async () => {
|
|
304
|
+
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
305
|
+
// A(顶层),A 的 workflow call → SB
|
|
306
|
+
const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
|
|
307
|
+
rootSessionId: MAIN,
|
|
308
|
+
slug: 'a',
|
|
309
|
+
})
|
|
310
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-A', {
|
|
311
|
+
rootSessionId: MAIN,
|
|
312
|
+
agentName: 'explorer',
|
|
313
|
+
sessionFile: saA,
|
|
314
|
+
})
|
|
315
|
+
// B(顶层),B 的 workflow call → SA(指回 A,成环)
|
|
316
|
+
const saB = await writeSubagentSession(dir, '--main-cwd--', B_REAL, {
|
|
317
|
+
rootSessionId: MAIN,
|
|
318
|
+
slug: 'b',
|
|
319
|
+
})
|
|
320
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-B', {
|
|
321
|
+
rootSessionId: MAIN,
|
|
322
|
+
agentName: 'explorer',
|
|
323
|
+
sessionFile: saB,
|
|
324
|
+
})
|
|
325
|
+
const wfA = await writeWfState(dir, '--main-cwd--', RUN_A, [saB])
|
|
326
|
+
const wfB = await writeWfState(dir, '--main-cwd--', RUN_B, [saA])
|
|
327
|
+
await writeWfLink(saA, A_REAL, { runId: RUN_A, path: wfA })
|
|
328
|
+
await writeWfLink(saB, B_REAL, { runId: RUN_B, path: wfB })
|
|
329
|
+
|
|
330
|
+
const tree = await buildExecutionTree(MAIN, dir)
|
|
331
|
+
|
|
332
|
+
// 环检测:truncated=true,不抛错
|
|
333
|
+
expect(tree.truncated).toBe(true)
|
|
334
|
+
// root.children 含 A, B(正常分支继续建树)
|
|
335
|
+
expect(tree.root.children).toHaveLength(2)
|
|
336
|
+
// A 下有 workflow-call 指向 B session;B 的 workflow 指回 A 被 visited 跳过(不无限递归)
|
|
337
|
+
const nodeA = findNode(tree.root, 'subagent', A_REAL)
|
|
338
|
+
expect(nodeA).toBeDefined()
|
|
339
|
+
// A 至少展开了 workflow-call(B session 的 call)
|
|
340
|
+
expect(nodeA!.children.some((c) => c.type === 'workflow-call')).toBe(true)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('TC-m3b-single-node:无 subagent record、无 workflow → 单节点树(ES5)', async () => {
|
|
344
|
+
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
345
|
+
|
|
346
|
+
const tree = await buildExecutionTree(MAIN, dir)
|
|
347
|
+
|
|
348
|
+
expect(tree.root.type).toBe('main')
|
|
349
|
+
expect(tree.root.sessionId).toBe(MAIN)
|
|
350
|
+
expect(tree.root.children).toEqual([])
|
|
351
|
+
expect(tree.totalNodes).toBe(1)
|
|
352
|
+
expect(tree.maxDepth).toBe(0)
|
|
353
|
+
expect(tree.sourceMode).toBe('precise') // 无 record → 确定无后代(非回退),ES5
|
|
354
|
+
expect(tree.truncated).toBe(false)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('TC-m3b-source-priority:parentRecordId 三级数据源 manifest>identity>flat(DM4)', async () => {
|
|
358
|
+
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
359
|
+
// X:顶层 subagent(挂 main)
|
|
360
|
+
const saX = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
|
|
361
|
+
rootSessionId: MAIN,
|
|
362
|
+
slug: 'x',
|
|
363
|
+
})
|
|
364
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-X', {
|
|
365
|
+
rootSessionId: MAIN,
|
|
366
|
+
agentName: 'explorer',
|
|
367
|
+
sessionFile: saX,
|
|
368
|
+
})
|
|
369
|
+
// Y:manifest.parentRecordId=sa-X(数据源 ①,精确挂 X)
|
|
370
|
+
const saY = await writeSubagentSession(dir, '--main-cwd--', B_REAL, {
|
|
371
|
+
rootSessionId: MAIN,
|
|
372
|
+
slug: 'y',
|
|
373
|
+
})
|
|
374
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-Y', {
|
|
375
|
+
rootSessionId: MAIN,
|
|
376
|
+
agentName: 'worker',
|
|
377
|
+
sessionFile: saY,
|
|
378
|
+
parentRecordId: 'sa-X',
|
|
379
|
+
})
|
|
380
|
+
// Z:manifest 无 parentRecordId,但 identity.data.parentRecordId=sa-X(数据源 ②,精确挂 X)
|
|
381
|
+
const saZ = await writeSubagentSession(dir, '--main-cwd--', C_REAL, {
|
|
382
|
+
rootSessionId: MAIN,
|
|
383
|
+
slug: 'z',
|
|
384
|
+
parentRecordId: 'sa-X', // 写入 identity.data.parentRecordId
|
|
385
|
+
})
|
|
386
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-Z', {
|
|
387
|
+
rootSessionId: MAIN,
|
|
388
|
+
agentName: 'worker',
|
|
389
|
+
sessionFile: saZ,
|
|
390
|
+
// manifest 无 parentRecordId → 走 ② identity
|
|
391
|
+
})
|
|
392
|
+
// W:manifest + identity 都无 parentRecordId(数据源 ③,flat 挂 main)
|
|
393
|
+
const saW = await writeSubagentSession(dir, '--main-cwd--', '000000000005', {
|
|
394
|
+
rootSessionId: MAIN,
|
|
395
|
+
slug: 'w',
|
|
396
|
+
})
|
|
397
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-W', {
|
|
398
|
+
rootSessionId: MAIN,
|
|
399
|
+
agentName: 'worker',
|
|
400
|
+
sessionFile: saW,
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
const tree = await buildExecutionTree(MAIN, dir)
|
|
404
|
+
|
|
405
|
+
// sourceMode=precise(Y/Z 有精确链)
|
|
406
|
+
expect(tree.sourceMode).toBe('precise')
|
|
407
|
+
// X 挂 main(顶层)
|
|
408
|
+
const nodeX = findNode(tree.root, 'subagent', A_REAL)
|
|
409
|
+
expect(nodeX).toBeDefined()
|
|
410
|
+
expect(tree.root.children.some((c) => c === nodeX)).toBe(true)
|
|
411
|
+
// Y 挂 X(manifest.parentRecordId=sa-X,①)
|
|
412
|
+
const nodeY = findNode(tree.root, 'subagent', B_REAL)
|
|
413
|
+
expect(nodeY).toBeDefined()
|
|
414
|
+
expect(nodeX!.children.some((c) => c === nodeY)).toBe(true)
|
|
415
|
+
expect(nodeY!.parentRecordId).toBe('sa-X')
|
|
416
|
+
// Z 挂 X(identity.data.parentRecordId=sa-X,②)
|
|
417
|
+
const nodeZ = findNode(tree.root, 'subagent', C_REAL)
|
|
418
|
+
expect(nodeZ).toBeDefined()
|
|
419
|
+
expect(nodeX!.children.some((c) => c === nodeZ)).toBe(true)
|
|
420
|
+
expect(nodeZ!.parentRecordId).toBe('sa-X')
|
|
421
|
+
// W 挂 main(flat,③)
|
|
422
|
+
const nodeW = findNode(tree.root, 'subagent', '000000000005')
|
|
423
|
+
expect(nodeW).toBeDefined()
|
|
424
|
+
expect(tree.root.children.some((c) => c === nodeW)).toBe(true)
|
|
425
|
+
expect(nodeW!.parentRecordId).toBeUndefined()
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('TC-m3b-mf1-dedup:call session 有 manifest(parentRecordId 链 + wf 指针双命中)只出现一次(MF-1)', async () => {
|
|
429
|
+
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
430
|
+
// A(顶层,parentRecordId=undefined),sessionFile 含 workflow-state-link → call session B
|
|
431
|
+
const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
|
|
432
|
+
rootSessionId: MAIN,
|
|
433
|
+
slug: 'sub-a',
|
|
434
|
+
})
|
|
435
|
+
const recAId = 'sa-record-A'
|
|
436
|
+
await writeRecordManifest(dir, '--main-cwd--', recAId, {
|
|
437
|
+
rootSessionId: MAIN,
|
|
438
|
+
agentName: 'explorer',
|
|
439
|
+
sessionFile: saA,
|
|
440
|
+
status: 'completed',
|
|
441
|
+
parentRecordId: undefined,
|
|
442
|
+
})
|
|
443
|
+
// B:A 的 subagent 后代(parentRecordId=recA)+ A 的 workflow call 目标(双命中,MF-1 场景)
|
|
444
|
+
const sbB = join(
|
|
445
|
+
dir,
|
|
446
|
+
'subagents',
|
|
447
|
+
'--main-cwd--',
|
|
448
|
+
'sessions',
|
|
449
|
+
`2026-08-07T16-49-48-393Z_${B_REAL}.jsonl`,
|
|
450
|
+
)
|
|
451
|
+
await mkdir(join(dir, 'subagents', '--main-cwd--', 'sessions'), { recursive: true })
|
|
452
|
+
await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
|
|
453
|
+
const recBId = 'sa-record-B'
|
|
454
|
+
await writeRecordManifest(dir, '--main-cwd--', recBId, {
|
|
455
|
+
rootSessionId: MAIN,
|
|
456
|
+
agentName: 'worker',
|
|
457
|
+
sessionFile: sbB,
|
|
458
|
+
status: 'completed',
|
|
459
|
+
parentRecordId: recAId,
|
|
460
|
+
})
|
|
461
|
+
// D:B 的 subagent 后代(验证 call session 内派生的嵌套 sub-subagent 仍正确挂到 B 下)
|
|
462
|
+
const saD = await writeSubagentSession(dir, '--main-cwd--', D_REAL, {
|
|
463
|
+
rootSessionId: MAIN,
|
|
464
|
+
slug: 'sub-d',
|
|
465
|
+
})
|
|
466
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-record-D', {
|
|
467
|
+
rootSessionId: MAIN,
|
|
468
|
+
agentName: 'worker',
|
|
469
|
+
sessionFile: saD,
|
|
470
|
+
status: 'completed',
|
|
471
|
+
parentRecordId: recBId,
|
|
472
|
+
})
|
|
473
|
+
// A 的 workflow-state-link → wf-state → calls[0].sessionFile = sbB
|
|
474
|
+
const wfPath = await writeWfState(dir, '--main-cwd--', RUN_A, [sbB])
|
|
475
|
+
await writeWfLink(saA, A_REAL, { runId: RUN_A, path: wfPath })
|
|
476
|
+
|
|
477
|
+
const tree = await buildExecutionTree(MAIN, dir)
|
|
478
|
+
|
|
479
|
+
// B 作为 subagent 出现一次(parentRecordId 链命中)
|
|
480
|
+
const nodeBSub = findNode(tree.root, 'subagent', B_REAL)
|
|
481
|
+
expect(nodeBSub).toBeDefined()
|
|
482
|
+
expect(nodeBSub!.parentRecordId).toBe(recAId)
|
|
483
|
+
// B 不作为 workflow-call 重复出现(MF-1 dedup)
|
|
484
|
+
const nodeBWf = findNode(tree.root, 'workflow-call', B_REAL)
|
|
485
|
+
expect(nodeBWf).toBeUndefined()
|
|
486
|
+
// B 挂在 A 下;A 下不应有指向 B 的 workflow-call
|
|
487
|
+
const nodeA = findNode(tree.root, 'subagent', A_REAL)
|
|
488
|
+
expect(nodeA).toBeDefined()
|
|
489
|
+
expect(nodeA!.children.some((c) => c === nodeBSub)).toBe(true)
|
|
490
|
+
expect(
|
|
491
|
+
nodeA!.children.some((c) => c.type === 'workflow-call' && c.sessionId === B_REAL),
|
|
492
|
+
).toBe(false)
|
|
493
|
+
// B 的嵌套后代 D 仍挂在 B 下(caution:call 节点内派生的 sub-subagent 不丢失)
|
|
494
|
+
const nodeD = findNode(tree.root, 'subagent', D_REAL)
|
|
495
|
+
expect(nodeD).toBeDefined()
|
|
496
|
+
expect(nodeBSub!.children.some((c) => c === nodeD)).toBe(true)
|
|
497
|
+
// 树规模:main + A + B + D = 4(B 不重复计数)
|
|
498
|
+
expect(tree.totalNodes).toBe(4)
|
|
499
|
+
expect(tree.truncated).toBe(false)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('TC-m3b-mf2-subtree:buildExecutionTree(subagentId) 切该 subagent 子树(MF-2,§5.5)', async () => {
|
|
503
|
+
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
504
|
+
// A(顶层)+ X(顶层,A 的兄弟,不应进 A 的子树)
|
|
505
|
+
const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
|
|
506
|
+
rootSessionId: MAIN,
|
|
507
|
+
slug: 'sub-a',
|
|
508
|
+
})
|
|
509
|
+
const recAId = 'sa-record-A'
|
|
510
|
+
await writeRecordManifest(dir, '--main-cwd--', recAId, {
|
|
511
|
+
rootSessionId: MAIN,
|
|
512
|
+
agentName: 'explorer',
|
|
513
|
+
sessionFile: saA,
|
|
514
|
+
status: 'completed',
|
|
515
|
+
parentRecordId: undefined,
|
|
516
|
+
})
|
|
517
|
+
const saX = await writeSubagentSession(dir, '--main-cwd--', X_REAL, {
|
|
518
|
+
rootSessionId: MAIN,
|
|
519
|
+
slug: 'sub-x',
|
|
520
|
+
})
|
|
521
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-record-X', {
|
|
522
|
+
rootSessionId: MAIN,
|
|
523
|
+
agentName: 'explorer',
|
|
524
|
+
sessionFile: saX,
|
|
525
|
+
status: 'completed',
|
|
526
|
+
parentRecordId: undefined,
|
|
527
|
+
})
|
|
528
|
+
// B(A 的后代),C(B 的后代)
|
|
529
|
+
const saB = await writeSubagentSession(dir, '--main-cwd--', B_REAL, {
|
|
530
|
+
rootSessionId: MAIN,
|
|
531
|
+
slug: 'sub-b',
|
|
532
|
+
})
|
|
533
|
+
const recBId = 'sa-record-B'
|
|
534
|
+
await writeRecordManifest(dir, '--main-cwd--', recBId, {
|
|
535
|
+
rootSessionId: MAIN,
|
|
536
|
+
agentName: 'worker',
|
|
537
|
+
sessionFile: saB,
|
|
538
|
+
status: 'completed',
|
|
539
|
+
parentRecordId: recAId,
|
|
540
|
+
})
|
|
541
|
+
const saC = await writeSubagentSession(dir, '--main-cwd--', C_REAL, {
|
|
542
|
+
rootSessionId: MAIN,
|
|
543
|
+
slug: 'sub-c',
|
|
544
|
+
})
|
|
545
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-record-C', {
|
|
546
|
+
rootSessionId: MAIN,
|
|
547
|
+
agentName: 'worker',
|
|
548
|
+
sessionFile: saC,
|
|
549
|
+
status: 'completed',
|
|
550
|
+
parentRecordId: recBId,
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
// 切 A 的子树(subagent root)
|
|
554
|
+
const tree = await buildExecutionTree(A_REAL, dir)
|
|
555
|
+
|
|
556
|
+
// root 是 A(subagent 节点,携带 manifest 元数据)
|
|
557
|
+
expect(tree.root.type).toBe('subagent')
|
|
558
|
+
expect(tree.root.sessionId).toBe(A_REAL)
|
|
559
|
+
expect(tree.root.agentName).toBe('explorer')
|
|
560
|
+
// A 的子树含 B→C,不含兄弟 X
|
|
561
|
+
const nodeB = findNode(tree.root, 'subagent', B_REAL)
|
|
562
|
+
expect(nodeB).toBeDefined()
|
|
563
|
+
expect(tree.root.children.some((c) => c === nodeB)).toBe(true)
|
|
564
|
+
const nodeC = findNode(tree.root, 'subagent', C_REAL)
|
|
565
|
+
expect(nodeC).toBeDefined()
|
|
566
|
+
expect(nodeB!.children.some((c) => c === nodeC)).toBe(true)
|
|
567
|
+
// 兄弟 X 不在 A 的子树
|
|
568
|
+
const nodeX = findNode(tree.root, 'subagent', X_REAL)
|
|
569
|
+
expect(nodeX).toBeUndefined()
|
|
570
|
+
// 树规模:A + B + C = 3
|
|
571
|
+
expect(tree.totalNodes).toBe(3)
|
|
572
|
+
expect(tree.sourceMode).toBe('precise')
|
|
573
|
+
expect(tree.truncated).toBe(false)
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('TC-m3b-mf1-main-root:main root 填 sessionFile 后 main 自身发起的 workflow run 入树(MF-1)', async () => {
|
|
577
|
+
const mainPath = await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
578
|
+
// main 的 workflow-state-link → wf-state → call session B
|
|
579
|
+
const sbB = join(
|
|
580
|
+
dir,
|
|
581
|
+
'subagents',
|
|
582
|
+
'--main-cwd--',
|
|
583
|
+
'sessions',
|
|
584
|
+
`2026-08-07T16-49-48-393Z_${B_REAL}.jsonl`,
|
|
585
|
+
)
|
|
586
|
+
await mkdir(join(dir, 'subagents', '--main-cwd--', 'sessions'), { recursive: true })
|
|
587
|
+
await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
|
|
588
|
+
const wfPath = await writeWfState(dir, '--main-cwd--', RUN_A, [sbB])
|
|
589
|
+
await writeWfLink(mainPath, MAIN, { runId: RUN_A, path: wfPath })
|
|
590
|
+
|
|
591
|
+
const tree = await buildExecutionTree(MAIN, dir, mainPath)
|
|
592
|
+
|
|
593
|
+
// main root 携带 sessionFile,workflow-state-link 被读取 → wf-call 子节点入树
|
|
594
|
+
expect(tree.root.type).toBe('main')
|
|
595
|
+
expect(tree.root.sessionFile).toBe(mainPath)
|
|
596
|
+
const nodeB = findNode(tree.root, 'workflow-call', B_REAL)
|
|
597
|
+
expect(nodeB).toBeDefined()
|
|
598
|
+
expect(tree.root.children.some((c) => c === nodeB)).toBe(true)
|
|
599
|
+
expect(nodeB!.runId).toBe(RUN_A)
|
|
600
|
+
expect(tree.totalNodes).toBe(2) // main + wf-call B
|
|
601
|
+
expect(tree.maxDepth).toBe(1)
|
|
602
|
+
expect(tree.truncated).toBe(false)
|
|
603
|
+
|
|
604
|
+
// 差分守卫:不传 mainSessionFile(旧行为)→ main root 无 sessionFile → workflow run 不可达
|
|
605
|
+
const tree2 = await buildExecutionTree(MAIN, dir)
|
|
606
|
+
expect(tree2.root.sessionFile).toBeUndefined()
|
|
607
|
+
expect(tree2.totalNodes).toBe(1)
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
it('TC-m3b-max-depth:21+ 层 parentRecordId 链在 MAX_DEPTH(20) 截断,truncated=true 不抛错(MF-4)', async () => {
|
|
611
|
+
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
612
|
+
// 循环生成 25 层链:R1 顶层(parentRecordId=undefined),R(i+1).parentRecordId=Ri.id
|
|
613
|
+
// 深度计数:root=0,R1=1,…,R20=20(attachSubagentChildren 在 node.depth>=20 截断,R21+ 不挂)
|
|
614
|
+
const CHAIN_RECORDS = 25
|
|
615
|
+
// 链 id 用独立前缀(0bbbbb…,与 MAIN 的 0aaaa… 不撞——否则 sidOf(1)===MAIN 会被
|
|
616
|
+
// rootManifest 探测误判为 subagent root,深度计数整体偏移 1)
|
|
617
|
+
const sidOf = (n: number): string =>
|
|
618
|
+
`0bbbbbbb-cccc-7ddd-eeee-0000000000${String(n).padStart(2, '0')}`
|
|
619
|
+
const recIdOf = (n: number): string => `sa-chain-${String(n).padStart(2, '0')}`
|
|
620
|
+
let prevRecId: string | undefined
|
|
621
|
+
for (let i = 1; i <= CHAIN_RECORDS; i++) {
|
|
622
|
+
const saPath = await writeSubagentSession(dir, '--main-cwd--', sidOf(i), {
|
|
623
|
+
rootSessionId: MAIN,
|
|
624
|
+
slug: `chain-${i}`,
|
|
625
|
+
})
|
|
626
|
+
await writeRecordManifest(dir, '--main-cwd--', recIdOf(i), {
|
|
627
|
+
rootSessionId: MAIN,
|
|
628
|
+
agentName: 'explorer',
|
|
629
|
+
sessionFile: saPath,
|
|
630
|
+
parentRecordId: prevRecId,
|
|
631
|
+
})
|
|
632
|
+
prevRecId = recIdOf(i)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const tree = await buildExecutionTree(MAIN, dir)
|
|
636
|
+
|
|
637
|
+
// 深度截断契约:MAX_DEPTH=20 → main + 20 层 subagent = 21 节点,R21+ 被截断
|
|
638
|
+
expect(tree.truncated).toBe(true)
|
|
639
|
+
expect(tree.maxDepth).toBe(20)
|
|
640
|
+
expect(tree.totalNodes).toBe(21)
|
|
641
|
+
// R20 是最后挂载的节点,其下无后代(R21 被截断)
|
|
642
|
+
const deepest = findNode(tree.root, 'subagent', sidOf(20))
|
|
643
|
+
expect(deepest).toBeDefined()
|
|
644
|
+
expect(deepest!.children).toEqual([])
|
|
645
|
+
// R21 不在树中
|
|
646
|
+
expect(findNode(tree.root, 'subagent', sidOf(21))).toBeUndefined()
|
|
647
|
+
expect(tree.root.children).toHaveLength(1)
|
|
648
|
+
expect(tree.sourceMode).toBe('precise')
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
it('TC-m3b-bfs-spread:旧机制嵌套形态 rootSessionId=父 session id,BFS 扩散收集不丢 record(MF-5)', async () => {
|
|
652
|
+
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
653
|
+
// A:顶层(rootSessionId=MAIN,无 parentRecordId——旧机制)
|
|
654
|
+
const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
|
|
655
|
+
rootSessionId: MAIN,
|
|
656
|
+
slug: 'a',
|
|
657
|
+
})
|
|
658
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-A', {
|
|
659
|
+
rootSessionId: MAIN,
|
|
660
|
+
agentName: 'explorer',
|
|
661
|
+
sessionFile: saA,
|
|
662
|
+
})
|
|
663
|
+
// B:旧机制嵌套形态——rootSessionId=A 的 session id(非 MAIN),A 无 manifest.parentRecordId
|
|
664
|
+
const saB = await writeSubagentSession(dir, '--main-cwd--', B_REAL, {
|
|
665
|
+
rootSessionId: A_REAL,
|
|
666
|
+
slug: 'b',
|
|
667
|
+
})
|
|
668
|
+
await writeRecordManifest(dir, '--main-cwd--', 'sa-B', {
|
|
669
|
+
rootSessionId: A_REAL,
|
|
670
|
+
agentName: 'explorer',
|
|
671
|
+
sessionFile: saB,
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
const tree = await buildExecutionTree(MAIN, dir)
|
|
675
|
+
|
|
676
|
+
// BFS 扩散:queue 从 MAIN → A 的 session id 入队 → 第二轮收 B。B 不被丢弃(ES3)
|
|
677
|
+
expect(tree.totalNodes).toBe(3) // main + A + B
|
|
678
|
+
expect(tree.sourceMode).toBe('flat-fallback')
|
|
679
|
+
const nodeA = findNode(tree.root, 'subagent', A_REAL)
|
|
680
|
+
expect(nodeA).toBeDefined()
|
|
681
|
+
const nodeB = findNode(tree.root, 'subagent', B_REAL)
|
|
682
|
+
expect(nodeB).toBeDefined()
|
|
683
|
+
// 旧机制无 parentRecordId → B 与 A 都 flat 挂 main(flat-fallback 语义)
|
|
684
|
+
expect(tree.root.children.some((c) => c === nodeA)).toBe(true)
|
|
685
|
+
expect(tree.root.children.some((c) => c === nodeB)).toBe(true)
|
|
686
|
+
expect(tree.truncated).toBe(false)
|
|
687
|
+
})
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
// ============================================================
|
|
691
|
+
// 真实数据守卫(CI 无本机 ~/.pi/agent → skip)
|
|
692
|
+
// ============================================================
|
|
693
|
+
|
|
694
|
+
describe.skipIf(!HAS_REAL)('buildExecutionTree - 真实数据守卫', () => {
|
|
695
|
+
it('TC-m3b-real-data-guard:本机旧机制数据 flat 回退,不抛错,totalNodes>1', async () => {
|
|
696
|
+
// 取一个真实存在的 main session(FAM 是 fork 家族根)
|
|
697
|
+
const FAM = '019fe620-8ae1-78a7-b76a-43a1ba4cc3c7'
|
|
698
|
+
const tree = await buildExecutionTree(FAM, REAL_AGENT_DIR)
|
|
699
|
+
// 旧机制:sourceMode='flat-fallback'(全无 parentRecordId)
|
|
700
|
+
expect(tree.sourceMode).toBe('flat-fallback')
|
|
701
|
+
// 有 subagent 后代(本机 3610 record,FAM 树非空)
|
|
702
|
+
expect(tree.totalNodes).toBeGreaterThan(1)
|
|
703
|
+
// 不抛错(已隐含:到这行说明成功)
|
|
704
|
+
expect(tree.root.type).toBe('main')
|
|
705
|
+
expect(tree.root.sessionId).toBe(FAM)
|
|
706
|
+
})
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
// ============================================================
|
|
710
|
+
// formatExecutionTreeText 渲染(IF5)
|
|
711
|
+
// ============================================================
|
|
712
|
+
|
|
713
|
+
describe('formatExecutionTreeText', () => {
|
|
714
|
+
it('渲染头部摘要 + 树形 + 尾部 👉 指引', () => {
|
|
715
|
+
const tree = {
|
|
716
|
+
root: {
|
|
717
|
+
type: 'main' as const,
|
|
718
|
+
sessionId: MAIN,
|
|
719
|
+
depth: 0,
|
|
720
|
+
rootSessionId: MAIN,
|
|
721
|
+
children: [
|
|
722
|
+
{
|
|
723
|
+
type: 'subagent' as const,
|
|
724
|
+
sessionId: A_REAL,
|
|
725
|
+
depth: 1,
|
|
726
|
+
rootSessionId: MAIN,
|
|
727
|
+
status: 'completed',
|
|
728
|
+
slug: 'sub-a',
|
|
729
|
+
task: 'do something',
|
|
730
|
+
children: [],
|
|
731
|
+
},
|
|
732
|
+
],
|
|
733
|
+
},
|
|
734
|
+
totalNodes: 2,
|
|
735
|
+
maxDepth: 1,
|
|
736
|
+
truncated: false,
|
|
737
|
+
sourceMode: 'precise' as const,
|
|
738
|
+
}
|
|
739
|
+
const text = formatExecutionTreeText(tree)
|
|
740
|
+
// 头部摘要
|
|
741
|
+
expect(text).toContain('2 node(s)')
|
|
742
|
+
expect(text).toContain('maxDepth 1')
|
|
743
|
+
expect(text).toContain('precise')
|
|
744
|
+
// 树形:type + sessionId 截断 + status + slug + task
|
|
745
|
+
expect(text).toContain('main')
|
|
746
|
+
expect(text).toContain('subagent')
|
|
747
|
+
expect(text).toContain(A_REAL.slice(0, 8))
|
|
748
|
+
expect(text).toContain('[completed]')
|
|
749
|
+
expect(text).toContain('slug=sub-a')
|
|
750
|
+
expect(text).toContain('do something')
|
|
751
|
+
// 尾部 👉 指引
|
|
752
|
+
expect(text).toContain('👉')
|
|
753
|
+
expect(text).toContain("outline")
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
it('flat-fallback 模式注明精度限制', () => {
|
|
757
|
+
const tree = {
|
|
758
|
+
root: {
|
|
759
|
+
type: 'main' as const,
|
|
760
|
+
sessionId: MAIN,
|
|
761
|
+
depth: 0,
|
|
762
|
+
rootSessionId: MAIN,
|
|
763
|
+
children: [],
|
|
764
|
+
},
|
|
765
|
+
totalNodes: 1,
|
|
766
|
+
maxDepth: 0,
|
|
767
|
+
truncated: false,
|
|
768
|
+
sourceMode: 'flat-fallback' as const,
|
|
769
|
+
}
|
|
770
|
+
const text = formatExecutionTreeText(tree)
|
|
771
|
+
expect(text).toContain('flat-fallback')
|
|
772
|
+
expect(text).toContain('旧机制')
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
it('truncated 模式标注截断', () => {
|
|
776
|
+
const tree = {
|
|
777
|
+
root: {
|
|
778
|
+
type: 'main' as const,
|
|
779
|
+
sessionId: MAIN,
|
|
780
|
+
depth: 0,
|
|
781
|
+
rootSessionId: MAIN,
|
|
782
|
+
children: [],
|
|
783
|
+
},
|
|
784
|
+
totalNodes: 1,
|
|
785
|
+
maxDepth: 0,
|
|
786
|
+
truncated: true,
|
|
787
|
+
sourceMode: 'precise' as const,
|
|
788
|
+
}
|
|
789
|
+
const text = formatExecutionTreeText(tree)
|
|
790
|
+
expect(text).toContain('truncated')
|
|
791
|
+
})
|
|
792
|
+
})
|