@zhushanwen/pi-session-reader 0.11.0 → 0.12.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 +5 -5
- package/src/__tests__/cross-package-subagent-core.test.ts +34 -25
- package/src/__tests__/entry-anchor.test.ts +3 -3
- package/src/__tests__/execution-tree.test.ts +85 -86
- package/src/__tests__/family-zcode.test.ts +24 -14
- package/src/__tests__/family.test.ts +4 -4
- package/src/__tests__/find.test.ts +50 -5
- package/src/__tests__/index.test.ts +3 -3
- package/src/__tests__/rebuild-manifest-dual-write.test.ts +29 -22
- package/src/__tests__/render.test.ts +2 -2
- package/src/__tests__/result.test.ts +20 -11
- package/src/__tests__/roots.test.ts +64 -26
- package/src/__tests__/subagents.test.ts +108 -52
- package/src/__tests__/tool-handler.test.ts +133 -83
- package/src/__tests__/workflow.test.ts +5 -5
- package/src/__tests__/zcode-manifest.test.ts +18 -10
- package/src/__tests__/zcode-routing.test.ts +21 -19
- package/src/__tests__/zcode-whitelist.test.ts +1 -1
- package/src/core/family.ts +6 -6
- package/src/core/render.ts +2 -2
- package/src/core/toolcall.ts +1 -1
- package/src/core/workflow.ts +9 -9
- package/src/discovery/entry-anchor.ts +5 -5
- package/src/discovery/find.ts +9 -1
- package/src/discovery/roots.ts +95 -7
- package/src/discovery/subagents.ts +24 -17
- package/src/discovery/whitelist.ts +4 -4
- package/src/discovery/workflows.ts +8 -8
- package/src/discovery/zcode-manifest.ts +22 -27
- package/src/extract.ts +1 -1
- package/src/index.ts +2 -2
- package/src/no-match.ts +1 -1
- package/src/result-action.ts +1 -1
- package/src/search-across.ts +3 -3
- package/src/tool-format.ts +12 -12
- package/src/tool-handler.ts +27 -19
- package/src/tui/session-command.ts +1 -1
- package/src/zcode-anchor-classify.ts +2 -2
|
@@ -3,6 +3,10 @@ import { tmpdir } from 'node:os'
|
|
|
3
3
|
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { buildExecutionTree, formatExecutionTreeText, type ExecutionTreeNode } from '../core/execution-tree.js'
|
|
6
|
+
import {
|
|
7
|
+
resolveStoreSubagentSessionsDir,
|
|
8
|
+
resolveStoreSubagentRecordsDir,
|
|
9
|
+
} from '../discovery/roots.js'
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* M3b U7 buildExecutionTree 单测(design m3b 6 testCases)。
|
|
@@ -17,7 +21,7 @@ import { buildExecutionTree, formatExecutionTreeText, type ExecutionTreeNode } f
|
|
|
17
21
|
* - TC-m3b-cycle-detection:workflow 指针环(A→B→A),visited Set 防环
|
|
18
22
|
* - TC-m3b-single-node:单节点树(无后代,ES5)
|
|
19
23
|
* - TC-m3b-source-priority:parentRecordId 三级数据源优先级(manifest>identity>flat,DM4)
|
|
20
|
-
* - TC-m3b-real-data-guard
|
|
24
|
+
* - TC-m3b-real-data-guard:真实形态数据检查(合成 fixture):mkdtemp 临时目录仿真真实
|
|
21
25
|
* agentDir 布局(时间戳前缀文件名 / 新旧机制混合 / 孤儿 manifest / 坏 manifest / 外来
|
|
22
26
|
* 家族),guard 语义断言——解析不抛错 / 树非空 / 结构自洽,不锁 sourceMode 具体值
|
|
23
27
|
*/
|
|
@@ -51,11 +55,11 @@ async function writeMainSession(dir: string, slug: string, id: string): Promise<
|
|
|
51
55
|
/**
|
|
52
56
|
* 写 subagent session 文件:首行 header(真实 id)+ 占位 message + 尾行 identity。
|
|
53
57
|
* identity 尾行含 rootSessionId/slug/agent,可选 parentRecordId(数据源 ② 测试用)。
|
|
54
|
-
* fileName 可选:默认 `<realId>.jsonl
|
|
58
|
+
* fileName 可选:默认 `<realId>.jsonl`;真实形态检查用例传时间戳前缀名(<ts>_<id>.jsonl)。
|
|
55
59
|
*/
|
|
56
60
|
async function writeSubagentSession(
|
|
57
61
|
dir: string,
|
|
58
|
-
|
|
62
|
+
rootSessionId: string,
|
|
59
63
|
realId: string,
|
|
60
64
|
identity: {
|
|
61
65
|
rootSessionId: string
|
|
@@ -66,7 +70,7 @@ async function writeSubagentSession(
|
|
|
66
70
|
},
|
|
67
71
|
fileName: string = `${realId}.jsonl`,
|
|
68
72
|
): Promise<string> {
|
|
69
|
-
const sessionDir =
|
|
73
|
+
const sessionDir = resolveStoreSubagentSessionsDir(dir, rootSessionId)
|
|
70
74
|
await mkdir(sessionDir, { recursive: true })
|
|
71
75
|
const path = join(sessionDir, fileName)
|
|
72
76
|
const data: Record<string, unknown> = {
|
|
@@ -80,7 +84,7 @@ async function writeSubagentSession(
|
|
|
80
84
|
}
|
|
81
85
|
if (identity.parentRecordId !== undefined) data.parentRecordId = identity.parentRecordId
|
|
82
86
|
const lines = [
|
|
83
|
-
JSON.stringify({ type: 'session', id: realId, cwd: `/proj/${
|
|
87
|
+
JSON.stringify({ type: 'session', id: realId, cwd: `/proj/${rootSessionId}` }),
|
|
84
88
|
JSON.stringify({
|
|
85
89
|
type: 'message',
|
|
86
90
|
id: 'm1',
|
|
@@ -96,7 +100,7 @@ async function writeSubagentSession(
|
|
|
96
100
|
/** 写 records manifest。fields 含必填 + 可选 parentRecordId(数据源 ①)。 */
|
|
97
101
|
async function writeRecordManifest(
|
|
98
102
|
dir: string,
|
|
99
|
-
|
|
103
|
+
rootSessionId: string,
|
|
100
104
|
id: string,
|
|
101
105
|
fields: {
|
|
102
106
|
rootSessionId: string
|
|
@@ -109,7 +113,7 @@ async function writeRecordManifest(
|
|
|
109
113
|
parentRecordId?: string
|
|
110
114
|
},
|
|
111
115
|
): Promise<void> {
|
|
112
|
-
const recordsDir =
|
|
116
|
+
const recordsDir = resolveStoreSubagentRecordsDir(dir, rootSessionId)
|
|
113
117
|
await mkdir(recordsDir, { recursive: true })
|
|
114
118
|
await writeFile(join(recordsDir, `${id}.json`), JSON.stringify({ id, ...fields }))
|
|
115
119
|
}
|
|
@@ -174,24 +178,26 @@ function findNode(
|
|
|
174
178
|
// ============================================================
|
|
175
179
|
|
|
176
180
|
describe('buildExecutionTree - fixture', () => {
|
|
177
|
-
let
|
|
181
|
+
let dataDir: string
|
|
182
|
+
let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
|
|
178
183
|
|
|
179
184
|
beforeEach(async () => {
|
|
180
|
-
|
|
185
|
+
dataDir = await makeAgentDir()
|
|
186
|
+
dir = join(dataDir, 'agent')
|
|
181
187
|
})
|
|
182
188
|
afterEach(async () => {
|
|
183
|
-
await rm(
|
|
189
|
+
await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
184
190
|
})
|
|
185
191
|
|
|
186
192
|
it('TC-m3b-nested-tree:main→subagent A→{workflow-call B, subagent C} 相互嵌套(parentRecordId 精确链)', async () => {
|
|
187
193
|
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
188
194
|
// subagent A(顶层,parentRecordId=undefined),sessionFile 含 workflow-state-link → call session B
|
|
189
|
-
const saA = await writeSubagentSession(dir,
|
|
195
|
+
const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
|
|
190
196
|
rootSessionId: MAIN,
|
|
191
197
|
slug: 'sub-a',
|
|
192
198
|
})
|
|
193
199
|
const recAId = 'sa-record-A'
|
|
194
|
-
await writeRecordManifest(dir,
|
|
200
|
+
await writeRecordManifest(dir, MAIN, recAId, {
|
|
195
201
|
rootSessionId: MAIN,
|
|
196
202
|
agentName: 'explorer',
|
|
197
203
|
sessionFile: saA,
|
|
@@ -199,11 +205,11 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
199
205
|
parentRecordId: undefined,
|
|
200
206
|
})
|
|
201
207
|
// subagent C(A 的后代,parentRecordId=recAId,精确链)
|
|
202
|
-
const saC = await writeSubagentSession(dir,
|
|
208
|
+
const saC = await writeSubagentSession(dir, MAIN, C_REAL, {
|
|
203
209
|
rootSessionId: MAIN,
|
|
204
210
|
slug: 'sub-c',
|
|
205
211
|
})
|
|
206
|
-
await writeRecordManifest(dir,
|
|
212
|
+
await writeRecordManifest(dir, MAIN, 'sa-record-C', {
|
|
207
213
|
rootSessionId: MAIN,
|
|
208
214
|
agentName: 'worker',
|
|
209
215
|
sessionFile: saC,
|
|
@@ -212,14 +218,11 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
212
218
|
})
|
|
213
219
|
// workflow-call B:A 的 sessionFile 的 v2 注册条目 → record 流 → calls[].sessionFile = SB
|
|
214
220
|
const sbB = join(
|
|
215
|
-
dir,
|
|
216
|
-
'subagents',
|
|
217
|
-
'--main-cwd--',
|
|
218
|
-
'sessions',
|
|
221
|
+
resolveStoreSubagentSessionsDir(dir, MAIN),
|
|
219
222
|
`2026-08-07T16-49-48-393Z_${B_REAL}.jsonl`,
|
|
220
223
|
)
|
|
221
224
|
// B call session 文件需存在(resolveWorkflows 读 call sessionFile;这里只需路径,不读内容建树)
|
|
222
|
-
await mkdir(
|
|
225
|
+
await mkdir(resolveStoreSubagentSessionsDir(dir, MAIN), { recursive: true })
|
|
223
226
|
await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
|
|
224
227
|
await writeWfV2Run(dir, '--main-cwd--', saA, A_REAL, RUN_A, [sbB])
|
|
225
228
|
|
|
@@ -252,29 +255,29 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
252
255
|
it('TC-m3b-flat-fallback:旧机制全无 parentRecordId → 全挂 main(扁平)+ flat-fallback', async () => {
|
|
253
256
|
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
254
257
|
// 3 个顶层 subagent,全无 parentRecordId(旧机制)
|
|
255
|
-
const saA = await writeSubagentSession(dir,
|
|
258
|
+
const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
|
|
256
259
|
rootSessionId: MAIN,
|
|
257
260
|
slug: 'a',
|
|
258
261
|
})
|
|
259
|
-
await writeRecordManifest(dir,
|
|
262
|
+
await writeRecordManifest(dir, MAIN, 'sa-A', {
|
|
260
263
|
rootSessionId: MAIN,
|
|
261
264
|
agentName: 'explorer',
|
|
262
265
|
sessionFile: saA,
|
|
263
266
|
})
|
|
264
|
-
const saB = await writeSubagentSession(dir,
|
|
267
|
+
const saB = await writeSubagentSession(dir, MAIN, B_REAL, {
|
|
265
268
|
rootSessionId: MAIN,
|
|
266
269
|
slug: 'b',
|
|
267
270
|
})
|
|
268
|
-
await writeRecordManifest(dir,
|
|
271
|
+
await writeRecordManifest(dir, MAIN, 'sa-B', {
|
|
269
272
|
rootSessionId: MAIN,
|
|
270
273
|
agentName: 'explorer',
|
|
271
274
|
sessionFile: saB,
|
|
272
275
|
})
|
|
273
|
-
const saC = await writeSubagentSession(dir,
|
|
276
|
+
const saC = await writeSubagentSession(dir, MAIN, C_REAL, {
|
|
274
277
|
rootSessionId: MAIN,
|
|
275
278
|
slug: 'c',
|
|
276
279
|
})
|
|
277
|
-
await writeRecordManifest(dir,
|
|
280
|
+
await writeRecordManifest(dir, MAIN, 'sa-C', {
|
|
278
281
|
rootSessionId: MAIN,
|
|
279
282
|
agentName: 'explorer',
|
|
280
283
|
sessionFile: saC,
|
|
@@ -294,21 +297,21 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
294
297
|
it('TC-m3b-cycle-detection:workflow 指针环 A→B→A,visited Set 防环 + truncated', async () => {
|
|
295
298
|
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
296
299
|
// A(顶层),A 的 workflow call → SB
|
|
297
|
-
const saA = await writeSubagentSession(dir,
|
|
300
|
+
const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
|
|
298
301
|
rootSessionId: MAIN,
|
|
299
302
|
slug: 'a',
|
|
300
303
|
})
|
|
301
|
-
await writeRecordManifest(dir,
|
|
304
|
+
await writeRecordManifest(dir, MAIN, 'sa-A', {
|
|
302
305
|
rootSessionId: MAIN,
|
|
303
306
|
agentName: 'explorer',
|
|
304
307
|
sessionFile: saA,
|
|
305
308
|
})
|
|
306
309
|
// B(顶层),B 的 workflow call → SA(指回 A,成环)
|
|
307
|
-
const saB = await writeSubagentSession(dir,
|
|
310
|
+
const saB = await writeSubagentSession(dir, MAIN, B_REAL, {
|
|
308
311
|
rootSessionId: MAIN,
|
|
309
312
|
slug: 'b',
|
|
310
313
|
})
|
|
311
|
-
await writeRecordManifest(dir,
|
|
314
|
+
await writeRecordManifest(dir, MAIN, 'sa-B', {
|
|
312
315
|
rootSessionId: MAIN,
|
|
313
316
|
agentName: 'explorer',
|
|
314
317
|
sessionFile: saB,
|
|
@@ -346,44 +349,44 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
346
349
|
it('TC-m3b-source-priority:parentRecordId 三级数据源 manifest>identity>flat(DM4)', async () => {
|
|
347
350
|
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
348
351
|
// X:顶层 subagent(挂 main)
|
|
349
|
-
const saX = await writeSubagentSession(dir,
|
|
352
|
+
const saX = await writeSubagentSession(dir, MAIN, A_REAL, {
|
|
350
353
|
rootSessionId: MAIN,
|
|
351
354
|
slug: 'x',
|
|
352
355
|
})
|
|
353
|
-
await writeRecordManifest(dir,
|
|
356
|
+
await writeRecordManifest(dir, MAIN, 'sa-X', {
|
|
354
357
|
rootSessionId: MAIN,
|
|
355
358
|
agentName: 'explorer',
|
|
356
359
|
sessionFile: saX,
|
|
357
360
|
})
|
|
358
361
|
// Y:manifest.parentRecordId=sa-X(数据源 ①,精确挂 X)
|
|
359
|
-
const saY = await writeSubagentSession(dir,
|
|
362
|
+
const saY = await writeSubagentSession(dir, MAIN, B_REAL, {
|
|
360
363
|
rootSessionId: MAIN,
|
|
361
364
|
slug: 'y',
|
|
362
365
|
})
|
|
363
|
-
await writeRecordManifest(dir,
|
|
366
|
+
await writeRecordManifest(dir, MAIN, 'sa-Y', {
|
|
364
367
|
rootSessionId: MAIN,
|
|
365
368
|
agentName: 'worker',
|
|
366
369
|
sessionFile: saY,
|
|
367
370
|
parentRecordId: 'sa-X',
|
|
368
371
|
})
|
|
369
372
|
// Z:manifest 无 parentRecordId,但 identity.data.parentRecordId=sa-X(数据源 ②,精确挂 X)
|
|
370
|
-
const saZ = await writeSubagentSession(dir,
|
|
373
|
+
const saZ = await writeSubagentSession(dir, MAIN, C_REAL, {
|
|
371
374
|
rootSessionId: MAIN,
|
|
372
375
|
slug: 'z',
|
|
373
376
|
parentRecordId: 'sa-X', // 写入 identity.data.parentRecordId
|
|
374
377
|
})
|
|
375
|
-
await writeRecordManifest(dir,
|
|
378
|
+
await writeRecordManifest(dir, MAIN, 'sa-Z', {
|
|
376
379
|
rootSessionId: MAIN,
|
|
377
380
|
agentName: 'worker',
|
|
378
381
|
sessionFile: saZ,
|
|
379
382
|
// manifest 无 parentRecordId → 走 ② identity
|
|
380
383
|
})
|
|
381
384
|
// W:manifest + identity 都无 parentRecordId(数据源 ③,flat 挂 main)
|
|
382
|
-
const saW = await writeSubagentSession(dir,
|
|
385
|
+
const saW = await writeSubagentSession(dir, MAIN, '000000000005', {
|
|
383
386
|
rootSessionId: MAIN,
|
|
384
387
|
slug: 'w',
|
|
385
388
|
})
|
|
386
|
-
await writeRecordManifest(dir,
|
|
389
|
+
await writeRecordManifest(dir, MAIN, 'sa-W', {
|
|
387
390
|
rootSessionId: MAIN,
|
|
388
391
|
agentName: 'worker',
|
|
389
392
|
sessionFile: saW,
|
|
@@ -417,12 +420,12 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
417
420
|
it('TC-m3b-mf1-dedup:call session 有 manifest(parentRecordId 链 + wf 指针双命中)只出现一次(MF-1)', async () => {
|
|
418
421
|
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
419
422
|
// A(顶层,parentRecordId=undefined),sessionFile 含 workflow-state-link → call session B
|
|
420
|
-
const saA = await writeSubagentSession(dir,
|
|
423
|
+
const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
|
|
421
424
|
rootSessionId: MAIN,
|
|
422
425
|
slug: 'sub-a',
|
|
423
426
|
})
|
|
424
427
|
const recAId = 'sa-record-A'
|
|
425
|
-
await writeRecordManifest(dir,
|
|
428
|
+
await writeRecordManifest(dir, MAIN, recAId, {
|
|
426
429
|
rootSessionId: MAIN,
|
|
427
430
|
agentName: 'explorer',
|
|
428
431
|
sessionFile: saA,
|
|
@@ -431,16 +434,13 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
431
434
|
})
|
|
432
435
|
// B:A 的 subagent 后代(parentRecordId=recA)+ A 的 workflow call 目标(双命中,MF-1 场景)
|
|
433
436
|
const sbB = join(
|
|
434
|
-
dir,
|
|
435
|
-
'subagents',
|
|
436
|
-
'--main-cwd--',
|
|
437
|
-
'sessions',
|
|
437
|
+
resolveStoreSubagentSessionsDir(dir, MAIN),
|
|
438
438
|
`2026-08-07T16-49-48-393Z_${B_REAL}.jsonl`,
|
|
439
439
|
)
|
|
440
|
-
await mkdir(
|
|
440
|
+
await mkdir(resolveStoreSubagentSessionsDir(dir, MAIN), { recursive: true })
|
|
441
441
|
await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
|
|
442
442
|
const recBId = 'sa-record-B'
|
|
443
|
-
await writeRecordManifest(dir,
|
|
443
|
+
await writeRecordManifest(dir, MAIN, recBId, {
|
|
444
444
|
rootSessionId: MAIN,
|
|
445
445
|
agentName: 'worker',
|
|
446
446
|
sessionFile: sbB,
|
|
@@ -448,11 +448,11 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
448
448
|
parentRecordId: recAId,
|
|
449
449
|
})
|
|
450
450
|
// D:B 的 subagent 后代(验证 call session 内派生的嵌套 sub-subagent 仍正确挂到 B 下)
|
|
451
|
-
const saD = await writeSubagentSession(dir,
|
|
451
|
+
const saD = await writeSubagentSession(dir, MAIN, D_REAL, {
|
|
452
452
|
rootSessionId: MAIN,
|
|
453
453
|
slug: 'sub-d',
|
|
454
454
|
})
|
|
455
|
-
await writeRecordManifest(dir,
|
|
455
|
+
await writeRecordManifest(dir, MAIN, 'sa-record-D', {
|
|
456
456
|
rootSessionId: MAIN,
|
|
457
457
|
agentName: 'worker',
|
|
458
458
|
sessionFile: saD,
|
|
@@ -490,23 +490,23 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
490
490
|
it('TC-m3b-mf2-subtree:buildExecutionTree(subagentId) 切该 subagent 子树(MF-2,§5.5)', async () => {
|
|
491
491
|
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
492
492
|
// A(顶层)+ X(顶层,A 的兄弟,不应进 A 的子树)
|
|
493
|
-
const saA = await writeSubagentSession(dir,
|
|
493
|
+
const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
|
|
494
494
|
rootSessionId: MAIN,
|
|
495
495
|
slug: 'sub-a',
|
|
496
496
|
})
|
|
497
497
|
const recAId = 'sa-record-A'
|
|
498
|
-
await writeRecordManifest(dir,
|
|
498
|
+
await writeRecordManifest(dir, MAIN, recAId, {
|
|
499
499
|
rootSessionId: MAIN,
|
|
500
500
|
agentName: 'explorer',
|
|
501
501
|
sessionFile: saA,
|
|
502
502
|
status: 'completed',
|
|
503
503
|
parentRecordId: undefined,
|
|
504
504
|
})
|
|
505
|
-
const saX = await writeSubagentSession(dir,
|
|
505
|
+
const saX = await writeSubagentSession(dir, MAIN, X_REAL, {
|
|
506
506
|
rootSessionId: MAIN,
|
|
507
507
|
slug: 'sub-x',
|
|
508
508
|
})
|
|
509
|
-
await writeRecordManifest(dir,
|
|
509
|
+
await writeRecordManifest(dir, MAIN, 'sa-record-X', {
|
|
510
510
|
rootSessionId: MAIN,
|
|
511
511
|
agentName: 'explorer',
|
|
512
512
|
sessionFile: saX,
|
|
@@ -514,23 +514,23 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
514
514
|
parentRecordId: undefined,
|
|
515
515
|
})
|
|
516
516
|
// B(A 的后代),C(B 的后代)
|
|
517
|
-
const saB = await writeSubagentSession(dir,
|
|
517
|
+
const saB = await writeSubagentSession(dir, MAIN, B_REAL, {
|
|
518
518
|
rootSessionId: MAIN,
|
|
519
519
|
slug: 'sub-b',
|
|
520
520
|
})
|
|
521
521
|
const recBId = 'sa-record-B'
|
|
522
|
-
await writeRecordManifest(dir,
|
|
522
|
+
await writeRecordManifest(dir, MAIN, recBId, {
|
|
523
523
|
rootSessionId: MAIN,
|
|
524
524
|
agentName: 'worker',
|
|
525
525
|
sessionFile: saB,
|
|
526
526
|
status: 'completed',
|
|
527
527
|
parentRecordId: recAId,
|
|
528
528
|
})
|
|
529
|
-
const saC = await writeSubagentSession(dir,
|
|
529
|
+
const saC = await writeSubagentSession(dir, MAIN, C_REAL, {
|
|
530
530
|
rootSessionId: MAIN,
|
|
531
531
|
slug: 'sub-c',
|
|
532
532
|
})
|
|
533
|
-
await writeRecordManifest(dir,
|
|
533
|
+
await writeRecordManifest(dir, MAIN, 'sa-record-C', {
|
|
534
534
|
rootSessionId: MAIN,
|
|
535
535
|
agentName: 'worker',
|
|
536
536
|
sessionFile: saC,
|
|
@@ -565,13 +565,10 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
565
565
|
const mainPath = await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
566
566
|
// main 的 v2 注册条目 → record 流 → call session B
|
|
567
567
|
const sbB = join(
|
|
568
|
-
dir,
|
|
569
|
-
'subagents',
|
|
570
|
-
'--main-cwd--',
|
|
571
|
-
'sessions',
|
|
568
|
+
resolveStoreSubagentSessionsDir(dir, MAIN),
|
|
572
569
|
`2026-08-07T16-49-48-393Z_${B_REAL}.jsonl`,
|
|
573
570
|
)
|
|
574
|
-
await mkdir(
|
|
571
|
+
await mkdir(resolveStoreSubagentSessionsDir(dir, MAIN), { recursive: true })
|
|
575
572
|
await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
|
|
576
573
|
await writeWfV2Run(dir, '--main-cwd--', mainPath, MAIN, RUN_A, [sbB])
|
|
577
574
|
|
|
@@ -588,7 +585,7 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
588
585
|
expect(tree.maxDepth).toBe(1)
|
|
589
586
|
expect(tree.truncated).toBe(false)
|
|
590
587
|
|
|
591
|
-
//
|
|
588
|
+
// 差分检查:不传 mainSessionFile(旧行为)→ main root 无 sessionFile → workflow run 不可达
|
|
592
589
|
const tree2 = await buildExecutionTree(MAIN, dir)
|
|
593
590
|
expect(tree2.root.sessionFile).toBeUndefined()
|
|
594
591
|
expect(tree2.totalNodes).toBe(1)
|
|
@@ -612,11 +609,11 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
612
609
|
const recIdOf = (n: number): string => `sa-chain-${String(n).padStart(2, '0')}`
|
|
613
610
|
let prevRecId: string | undefined
|
|
614
611
|
for (let i = 1; i <= CHAIN_RECORDS; i++) {
|
|
615
|
-
const saPath = await writeSubagentSession(dir,
|
|
612
|
+
const saPath = await writeSubagentSession(dir, MAIN, sidOf(i), {
|
|
616
613
|
rootSessionId: MAIN,
|
|
617
614
|
slug: `chain-${i}`,
|
|
618
615
|
})
|
|
619
|
-
await writeRecordManifest(dir,
|
|
616
|
+
await writeRecordManifest(dir, MAIN, recIdOf(i), {
|
|
620
617
|
rootSessionId: MAIN,
|
|
621
618
|
agentName: 'explorer',
|
|
622
619
|
sessionFile: saPath,
|
|
@@ -644,11 +641,11 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
644
641
|
it('TC-m3b-bfs-spread:旧机制嵌套形态 rootSessionId=父 session id,BFS 扩散收集不丢 record(MF-5)', async () => {
|
|
645
642
|
await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
646
643
|
// A:顶层(rootSessionId=MAIN,无 parentRecordId——旧机制)
|
|
647
|
-
const saA = await writeSubagentSession(dir,
|
|
644
|
+
const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
|
|
648
645
|
rootSessionId: MAIN,
|
|
649
646
|
slug: 'a',
|
|
650
647
|
})
|
|
651
|
-
await writeRecordManifest(dir,
|
|
648
|
+
await writeRecordManifest(dir, MAIN, 'sa-A', {
|
|
652
649
|
rootSessionId: MAIN,
|
|
653
650
|
agentName: 'explorer',
|
|
654
651
|
sessionFile: saA,
|
|
@@ -658,7 +655,7 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
658
655
|
rootSessionId: A_REAL,
|
|
659
656
|
slug: 'b',
|
|
660
657
|
})
|
|
661
|
-
await writeRecordManifest(dir,
|
|
658
|
+
await writeRecordManifest(dir, MAIN, 'sa-B', {
|
|
662
659
|
rootSessionId: A_REAL,
|
|
663
660
|
agentName: 'explorer',
|
|
664
661
|
sessionFile: saB,
|
|
@@ -681,18 +678,20 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
681
678
|
})
|
|
682
679
|
|
|
683
680
|
// ============================================================
|
|
684
|
-
//
|
|
681
|
+
// 真实形态数据检查(合成 fixture):mkdtemp 临时目录仿真真实 agentDir 布局,
|
|
685
682
|
// guard 语义断言(解析不抛错 / 树非空 / 结构自洽 / sourceMode 值域),不触碰真实数据目录。
|
|
686
683
|
// ============================================================
|
|
687
684
|
|
|
688
685
|
describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
|
|
689
|
-
let
|
|
686
|
+
let dataDir: string
|
|
687
|
+
let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
|
|
690
688
|
|
|
691
689
|
beforeEach(async () => {
|
|
692
|
-
|
|
690
|
+
dataDir = await makeAgentDir()
|
|
691
|
+
dir = join(dataDir, 'agent')
|
|
693
692
|
})
|
|
694
693
|
afterEach(async () => {
|
|
695
|
-
await rm(
|
|
694
|
+
await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
696
695
|
})
|
|
697
696
|
|
|
698
697
|
it('TC-m3b-real-data-guard:真实形态布局(时间戳文件名/新旧机制混合/孤儿 manifest/坏 manifest/外来家族)解析不抛错,树非空且结构自洽', async () => {
|
|
@@ -712,12 +711,12 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
|
|
|
712
711
|
// A:顶层 subagent(identity 尾行走数据源 ② 读路径,无 parentRecordId → 顶层挂 main)
|
|
713
712
|
const saA = await writeSubagentSession(
|
|
714
713
|
dir,
|
|
715
|
-
|
|
714
|
+
MAIN,
|
|
716
715
|
A_REAL,
|
|
717
716
|
{ rootSessionId: MAIN, slug: 'guard-a' },
|
|
718
717
|
prefixed(A_REAL),
|
|
719
718
|
)
|
|
720
|
-
await writeRecordManifest(dir,
|
|
719
|
+
await writeRecordManifest(dir, MAIN, 'rec-ga', {
|
|
721
720
|
rootSessionId: MAIN,
|
|
722
721
|
agentName: 'explorer',
|
|
723
722
|
sessionFile: saA,
|
|
@@ -727,12 +726,12 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
|
|
|
727
726
|
// B:A 的嵌套后代(manifest.parentRecordId 精确链,新机制)
|
|
728
727
|
const saB = await writeSubagentSession(
|
|
729
728
|
dir,
|
|
730
|
-
|
|
729
|
+
MAIN,
|
|
731
730
|
B_REAL,
|
|
732
731
|
{ rootSessionId: MAIN, slug: 'guard-b' },
|
|
733
732
|
prefixed(B_REAL),
|
|
734
733
|
)
|
|
735
|
-
await writeRecordManifest(dir,
|
|
734
|
+
await writeRecordManifest(dir, MAIN, 'rec-gb', {
|
|
736
735
|
rootSessionId: MAIN,
|
|
737
736
|
agentName: 'worker',
|
|
738
737
|
sessionFile: saB,
|
|
@@ -743,12 +742,12 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
|
|
|
743
742
|
// C:旧机制顶层 subagent(manifest/identity 均无 parentRecordId → flat 挂 main)
|
|
744
743
|
const saC = await writeSubagentSession(
|
|
745
744
|
dir,
|
|
746
|
-
|
|
745
|
+
MAIN,
|
|
747
746
|
C_REAL,
|
|
748
747
|
{ rootSessionId: MAIN, slug: 'guard-c' },
|
|
749
748
|
prefixed(C_REAL),
|
|
750
749
|
)
|
|
751
|
-
await writeRecordManifest(dir,
|
|
750
|
+
await writeRecordManifest(dir, MAIN, 'rec-gc', {
|
|
752
751
|
rootSessionId: MAIN,
|
|
753
752
|
agentName: 'explorer',
|
|
754
753
|
sessionFile: saC,
|
|
@@ -756,24 +755,24 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
|
|
|
756
755
|
status: 'completed',
|
|
757
756
|
})
|
|
758
757
|
// D:孤儿 manifest(sessionFile 指向已被 GC 的不存在文件——manifest 残留形态)
|
|
759
|
-
await writeRecordManifest(dir,
|
|
758
|
+
await writeRecordManifest(dir, MAIN, 'rec-gd', {
|
|
760
759
|
rootSessionId: MAIN,
|
|
761
760
|
agentName: 'worker',
|
|
762
|
-
sessionFile: join(dir,
|
|
761
|
+
sessionFile: join(resolveStoreSubagentSessionsDir(dir, MAIN), prefixed(D_REAL)),
|
|
763
762
|
status: 'completed',
|
|
764
763
|
})
|
|
765
764
|
// 坏 manifest(JSON 损坏,listRecordManifests 容错跳过不中断)
|
|
766
|
-
await mkdir(
|
|
767
|
-
await writeFile(join(dir,
|
|
765
|
+
await mkdir(resolveStoreSubagentRecordsDir(dir, MAIN), { recursive: true })
|
|
766
|
+
await writeFile(join(resolveStoreSubagentRecordsDir(dir, MAIN), 'broken.json'), '{oops')
|
|
768
767
|
// 外来家族 X:同 agentDir 内其他 main(OTHER_MAIN)的 subagent,不应泄漏进 MAIN 的树
|
|
769
768
|
const saX = await writeSubagentSession(
|
|
770
769
|
dir,
|
|
771
|
-
|
|
770
|
+
OTHER_MAIN,
|
|
772
771
|
X_REAL,
|
|
773
772
|
{ rootSessionId: OTHER_MAIN, slug: 'foreign-x' },
|
|
774
773
|
prefixed(X_REAL),
|
|
775
774
|
)
|
|
776
|
-
await writeRecordManifest(dir,
|
|
775
|
+
await writeRecordManifest(dir, MAIN, 'rec-gx', {
|
|
777
776
|
rootSessionId: OTHER_MAIN,
|
|
778
777
|
agentName: 'explorer',
|
|
779
778
|
sessionFile: saX,
|
|
@@ -786,16 +785,16 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
|
|
|
786
785
|
// 全无 → flat-fallback),不锁具体值
|
|
787
786
|
expect(['precise', 'flat-fallback']).toContain(tree.sourceMode)
|
|
788
787
|
// 家族树非空:main + 顶层 A/C/D + A 的嵌套后代 B = 5;外来 X 不泄漏(合成数据确定性
|
|
789
|
-
//
|
|
788
|
+
// 可数,精确计数即非泄漏检查)
|
|
790
789
|
expect(tree.totalNodes).toBe(5)
|
|
791
790
|
expect(tree.root.type).toBe('main')
|
|
792
791
|
expect(tree.root.sessionId).toBe(MAIN)
|
|
793
792
|
expect(findNode(tree.root, 'subagent', X_REAL)).toBeUndefined()
|
|
794
|
-
// 孤儿 D 仍入树(sessionFile 残留路径,不因文件缺失抛错/丢弃 record
|
|
795
|
-
//
|
|
793
|
+
// 孤儿 D 仍入树(sessionFile 残留路径,不因文件缺失抛错/丢弃 record——原真实数据检查
|
|
794
|
+
// 独有覆盖范围,合成化后保留)
|
|
796
795
|
const nodeD = findNode(tree.root, 'subagent', D_REAL)
|
|
797
796
|
expect(nodeD).toBeDefined()
|
|
798
|
-
expect(nodeD!.sessionFile).toBe(join(dir,
|
|
797
|
+
expect(nodeD!.sessionFile).toBe(join(resolveStoreSubagentSessionsDir(dir, MAIN), prefixed(D_REAL)))
|
|
799
798
|
// 树结构自洽(ExecutionTreeNode 契约,与具体数据形态无关):DFS 实际节点数 ===
|
|
800
799
|
// totalNodes;子节点 depth = 父 depth + 1;全树 rootSessionId 共享同一顶层 main
|
|
801
800
|
let count = 0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
4
|
+
import { join, dirname } from 'node:path'
|
|
5
5
|
import { DatabaseSync } from 'node:sqlite'
|
|
6
6
|
|
|
7
7
|
import { buildFamilyIndex, resolveFamily, buildZcodeSubagentsByRoot } from '../core/family.js'
|
|
@@ -17,7 +17,7 @@ import { listZcodeManifests } from '../discovery/zcode-manifest.js'
|
|
|
17
17
|
// 行被删 → 仍 false)即「列表视图不开库」的分工声明断言。
|
|
18
18
|
//
|
|
19
19
|
// 与 pi 判据不同构(D5-1 显式声明):pi = fileStats JSONL 存在性;zcode 节点无
|
|
20
|
-
// JSONL 文件,按「库可达」判。pi 侧既有 fixture 行为零变化由 family.test.ts
|
|
20
|
+
// JSONL 文件,按「库可达」判。pi 侧既有 fixture 行为零变化由 family.test.ts 全绿检查
|
|
21
21
|
// (本文件零改动 family.test.ts——更强的零涟漪证据)。
|
|
22
22
|
// ============================================================
|
|
23
23
|
|
|
@@ -47,13 +47,13 @@ function zcodeNode(opts: Partial<ZcodeFamilyNode> & { id: string }): ZcodeFamily
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
50
|
+
/** 在存储树分区 <root>/<rootSessionId>/subagents/records/ 下写 zcode manifest(F21 磁盘真实形态:无 sessionFile 键;[session-store-layout-inversion D6/单新根] 读侧已换存储树) */
|
|
51
51
|
function writeZcodeManifest(
|
|
52
52
|
agentDir: string,
|
|
53
53
|
saId: string,
|
|
54
54
|
sessionRef: Record<string, string>,
|
|
55
55
|
): void {
|
|
56
|
-
const recordsDir = join(agentDir, '
|
|
56
|
+
const recordsDir = join(dirname(agentDir), 'ext-sessions', ROOT, 'subagents', 'records')
|
|
57
57
|
mkdirSync(recordsDir, { recursive: true })
|
|
58
58
|
writeFileSync(
|
|
59
59
|
join(recordsDir, `${saId}.json`),
|
|
@@ -167,11 +167,15 @@ describe('core:buildZcodeSubagentsByRoot / buildFamilyIndex 的 zcode 分支
|
|
|
167
167
|
|
|
168
168
|
describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(场景 2 两态 + 反向断言)', () => {
|
|
169
169
|
it('库可达态(dbPath 指向真实 fixture 库)→ cleanedUp=false', async () => {
|
|
170
|
+
// agentDir 两层化(<mkdtemp>/agent):存储树推导 = dirname(agentDir)/ext-sessions,
|
|
171
|
+
// 单层直接当 agentDir 会把扫描根泄到 tmpdir 公共层(跨测试污染——先例 reaper 同修)
|
|
172
|
+
const agentDir = join(dir, 'agent')
|
|
173
|
+
mkdirSync(agentDir, { recursive: true })
|
|
170
174
|
const dbPath = join(dir, 'isolated.sqlite')
|
|
171
175
|
createFixtureDb(dbPath, 'sess_live')
|
|
172
|
-
writeZcodeManifest(
|
|
176
|
+
writeZcodeManifest(agentDir, 'sa-live', { sessionId: 'sess_live', dbPath })
|
|
173
177
|
|
|
174
|
-
const nodes = await listZcodeManifests(
|
|
178
|
+
const nodes = await listZcodeManifests(agentDir)
|
|
175
179
|
expect(nodes).toHaveLength(1)
|
|
176
180
|
expect(nodes[0]?.dbFileExists).toBe(true)
|
|
177
181
|
|
|
@@ -181,12 +185,14 @@ describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(
|
|
|
181
185
|
})
|
|
182
186
|
|
|
183
187
|
it('库不存在态(dbPath 指向不存在路径)→ cleanedUp=true(GC 语义,不报错)', async () => {
|
|
184
|
-
|
|
188
|
+
const agentDir = join(dir, 'agent')
|
|
189
|
+
mkdirSync(agentDir, { recursive: true })
|
|
190
|
+
writeZcodeManifest(agentDir, 'sa-gone', {
|
|
185
191
|
sessionId: 'sess_gone',
|
|
186
192
|
dbPath: join(dir, 'subagents', 'proj-a', 'missing.sqlite'),
|
|
187
193
|
})
|
|
188
194
|
|
|
189
|
-
const nodes = await listZcodeManifests(
|
|
195
|
+
const nodes = await listZcodeManifests(agentDir)
|
|
190
196
|
expect(nodes[0]?.dbFileExists).toBe(false)
|
|
191
197
|
|
|
192
198
|
const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))
|
|
@@ -196,9 +202,11 @@ describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(
|
|
|
196
202
|
it('反向断言:库文件在但库内 session 行被删 → 仍 cleanedUp=false(列表视图不开库的分工声明)', async () => {
|
|
197
203
|
// 分工(D5-1):库内 session 已被 zcode GC 的情形由 read 路径 zcode_session_not_found
|
|
198
204
|
// 权威承接;family 只 stat 文件存在性。本用例删行留文件,断言 family 不受库内容影响。
|
|
205
|
+
const agentDir = join(dir, 'agent')
|
|
206
|
+
mkdirSync(agentDir, { recursive: true })
|
|
199
207
|
const dbPath = join(dir, 'gc.sqlite')
|
|
200
208
|
createFixtureDb(dbPath, 'sess_victim')
|
|
201
|
-
writeZcodeManifest(
|
|
209
|
+
writeZcodeManifest(agentDir, 'sa-gced', { sessionId: 'sess_victim', dbPath })
|
|
202
210
|
|
|
203
211
|
// 前置核实:fixture 库里确实有该 session 行(反向断言的非平凡性前提)
|
|
204
212
|
{
|
|
@@ -225,7 +233,7 @@ describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(
|
|
|
225
233
|
}
|
|
226
234
|
}
|
|
227
235
|
|
|
228
|
-
const nodes = await listZcodeManifests(
|
|
236
|
+
const nodes = await listZcodeManifests(agentDir)
|
|
229
237
|
expect(nodes[0]?.dbFileExists).toBe(true) // 只 stat:文件在即 true,不看内容
|
|
230
238
|
|
|
231
239
|
const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))
|
|
@@ -235,17 +243,19 @@ describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(
|
|
|
235
243
|
it('坏 manifest 与缺库节点混排 → 好节点照常输出(单节点不可达不炸整屏)', async () => {
|
|
236
244
|
const dbPath = join(dir, 'ok.sqlite')
|
|
237
245
|
createFixtureDb(dbPath, 'sess_ok')
|
|
238
|
-
|
|
246
|
+
const agentDir = join(dir, 'agent')
|
|
247
|
+
mkdirSync(agentDir, { recursive: true })
|
|
248
|
+
writeZcodeManifest(agentDir, 'sa-ok', { sessionId: 'sess_ok', dbPath })
|
|
239
249
|
// 缺库节点:合法 zcode 形态(dbFileExists=false,不抛错),不剔除出列表
|
|
240
|
-
writeZcodeManifest(
|
|
250
|
+
writeZcodeManifest(agentDir, 'sa-broken-manifest', { sessionId: 'sess_x', dbPath: '/nonexistent/x.sqlite' })
|
|
241
251
|
// 坏 JSON manifest:枚举跳过(不中断)
|
|
242
252
|
writeFileSync(
|
|
243
|
-
join(
|
|
253
|
+
join(dirname(agentDir), 'ext-sessions', ROOT, 'subagents', 'records', 'sa-corrupt.json'),
|
|
244
254
|
'{{{broken',
|
|
245
255
|
'utf8',
|
|
246
256
|
)
|
|
247
257
|
|
|
248
|
-
const nodes = await listZcodeManifests(
|
|
258
|
+
const nodes = await listZcodeManifests(agentDir)
|
|
249
259
|
expect(nodes.map((n) => n.id).sort()).toEqual(['sa-broken-manifest', 'sa-ok'])
|
|
250
260
|
|
|
251
261
|
const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))
|