@zhushanwen/pi-session-reader 0.1.0 → 0.2.1
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__/parser.test.ts +2 -1
- package/src/__tests__/real-data.ts +20 -0
- package/src/__tests__/render.test.ts +2 -1
- package/src/__tests__/subagents.test.ts +323 -14
- package/src/__tests__/tool-handler.test.ts +872 -5
- package/src/__tests__/tree.test.ts +20 -0
- package/src/__tests__/turns.test.ts +2 -1
- package/src/__tests__/workflow.test.ts +353 -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 +330 -0
- package/src/discovery/find.ts +167 -20
- package/src/discovery/roots.ts +3 -2
- package/src/discovery/subagents.ts +113 -152
- package/src/discovery/workflows.ts +164 -0
- package/src/index.ts +22 -4
- package/src/tool-handler.ts +334 -21
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
3
4
|
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
4
5
|
import { execSync } from 'node:child_process'
|
|
5
6
|
import { join } from 'node:path'
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildFamilyFromFs,
|
|
9
|
+
listRecordManifests,
|
|
10
|
+
extractSessionIdFromFilename,
|
|
11
|
+
type RecordManifest,
|
|
12
|
+
} from '../discovery/subagents.js'
|
|
7
13
|
|
|
8
14
|
// ---- fixture 常量(uuid 特征,满足 extractSessionIdFromFilename + 互不为子串)----
|
|
9
15
|
const ROOT = '0aaaaaaa-bbbb-7ccc-dddd-000000000001'
|
|
@@ -26,6 +32,21 @@ function hasRealSession(sid: string): boolean {
|
|
|
26
32
|
return false
|
|
27
33
|
}
|
|
28
34
|
}
|
|
35
|
+
|
|
36
|
+
// 双目录版探测:subagent session 文件在 subagents/ 下,hasRealSession 扫不到。
|
|
37
|
+
// 用于对活跃数据目录中具体文件(fork 子代/隔代 subagent)存在性的守卫。
|
|
38
|
+
function hasAnyRealSession(fragment: string): boolean {
|
|
39
|
+
try {
|
|
40
|
+
return (
|
|
41
|
+
execSync(
|
|
42
|
+
`find ${REAL_AGENT_DIR}/sessions ${REAL_AGENT_DIR}/subagents -name '*${fragment}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
43
|
+
{ encoding: 'utf8' },
|
|
44
|
+
).trim().length > 0
|
|
45
|
+
)
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
29
50
|
const HAS_REAL_ROOT = hasRealSession('019fe620-8ae1-78a7-b76a-43a1ba4cc3c7')
|
|
30
51
|
const HAS_REAL_WF = hasRealSession('019fdcda-75c7-74b7-a160-f67f6bf88384')
|
|
31
52
|
|
|
@@ -54,12 +75,13 @@ async function writeMainSession(
|
|
|
54
75
|
/**
|
|
55
76
|
* 写 subagent session 文件:首行 header(真实 id)+ 占位 message + 尾行 identity。
|
|
56
77
|
* identity 在尾行(实测 pi 行为;subagent-identity 由 session-runner 在 session 创建后写)。
|
|
78
|
+
* U4 扩展:identity.task/agent 可定制(P-fallback 测试用,默认 't'/'explorer')。
|
|
57
79
|
*/
|
|
58
80
|
async function writeSubagentSession(
|
|
59
81
|
dir: string,
|
|
60
82
|
slug: string,
|
|
61
|
-
|
|
62
|
-
identity: { rootSessionId: string; slug: string; dataId?: string },
|
|
83
|
+
realId: string,
|
|
84
|
+
identity: { rootSessionId: string; slug: string; dataId?: string; task?: string; agent?: string },
|
|
63
85
|
): Promise<string> {
|
|
64
86
|
const sessionDir = join(dir, 'subagents', slug, 'sessions')
|
|
65
87
|
await mkdir(sessionDir, { recursive: true })
|
|
@@ -79,9 +101,9 @@ async function writeSubagentSession(
|
|
|
79
101
|
id: identity.dataId ?? `sa-${realId.slice(0, 8)}`,
|
|
80
102
|
rootSessionId: identity.rootSessionId,
|
|
81
103
|
slug: identity.slug,
|
|
82
|
-
agent: 'explorer',
|
|
104
|
+
agent: identity.agent ?? 'explorer',
|
|
83
105
|
mode: 'sync',
|
|
84
|
-
task: 't',
|
|
106
|
+
task: identity.task ?? 't',
|
|
85
107
|
startedAt: 1,
|
|
86
108
|
},
|
|
87
109
|
}),
|
|
@@ -90,6 +112,31 @@ async function writeSubagentSession(
|
|
|
90
112
|
return path
|
|
91
113
|
}
|
|
92
114
|
|
|
115
|
+
/**
|
|
116
|
+
* 写 alive subagent 文件但**无 identity 尾行**(运行中/异常,尾行是 message)。
|
|
117
|
+
* TC-u4-pfallback-no-identity 场景:header 有效、无 manifest、无 identity → buildFamilyFromFs 跳过。
|
|
118
|
+
*/
|
|
119
|
+
async function writeAliveSubagentNoIdentity(
|
|
120
|
+
dir: string,
|
|
121
|
+
slug: string,
|
|
122
|
+
realId: string,
|
|
123
|
+
): Promise<string> {
|
|
124
|
+
const sessionDir = join(dir, 'subagents', slug, 'sessions')
|
|
125
|
+
await mkdir(sessionDir, { recursive: true })
|
|
126
|
+
const path = join(sessionDir, `${realId}.jsonl`)
|
|
127
|
+
const lines = [
|
|
128
|
+
JSON.stringify({ type: 'session', id: realId, cwd: `/proj/${slug}` }),
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
type: 'message',
|
|
131
|
+
id: 'm1',
|
|
132
|
+
parentId: realId,
|
|
133
|
+
message: { role: 'user', content: 'still running' },
|
|
134
|
+
}),
|
|
135
|
+
]
|
|
136
|
+
await writeFile(path, lines.join('\n') + '\n')
|
|
137
|
+
return path
|
|
138
|
+
}
|
|
139
|
+
|
|
93
140
|
/** 写 wf-state 文件(每行一个快照;字符串按原样写入,可注入坏行)。返回绝对路径。 */
|
|
94
141
|
async function writeWfState(dir: string, slug: string, lines: string[]): Promise<string> {
|
|
95
142
|
const wfDir = join(dir, 'sessions', slug, 'workflow-state')
|
|
@@ -118,12 +165,20 @@ async function writeWfLink(
|
|
|
118
165
|
await writeFile(sessionPath, line + '\n', { flag: 'a' })
|
|
119
166
|
}
|
|
120
167
|
|
|
121
|
-
/** 写 records manifest(孤儿源)。 */
|
|
168
|
+
/** 写 records manifest(孤儿源)。U4 扩展:fields 支持富字段 task/slug/model/status。 */
|
|
122
169
|
async function writeRecordManifest(
|
|
123
170
|
dir: string,
|
|
124
171
|
slug: string,
|
|
125
172
|
id: string,
|
|
126
|
-
fields: {
|
|
173
|
+
fields: {
|
|
174
|
+
rootSessionId: string
|
|
175
|
+
agentName?: string
|
|
176
|
+
sessionFile: string
|
|
177
|
+
task?: string
|
|
178
|
+
slug?: string
|
|
179
|
+
model?: string
|
|
180
|
+
status?: string
|
|
181
|
+
},
|
|
127
182
|
): Promise<void> {
|
|
128
183
|
const recordsDir = join(dir, 'subagents', slug, 'records')
|
|
129
184
|
await mkdir(recordsDir, { recursive: true })
|
|
@@ -362,7 +417,7 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
362
417
|
expect(family.workflows[0].calls[0].mtime).toBe(0)
|
|
363
418
|
})
|
|
364
419
|
|
|
365
|
-
it('MF-3 回归:alive 但无 identity 的 subagent
|
|
420
|
+
it('MF-3 回归:alive 但无 identity 的 subagent(运行中)不被收编为 cleanedUp——U4 后 manifest 主路径建族', async () => {
|
|
366
421
|
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
367
422
|
// 运行中 subagent:有效 header、无 identity 尾行(identity 完成时才写入),manifest 已存在
|
|
368
423
|
const subDir = join(dir, 'subagents', '--root-cwd--', 'sessions')
|
|
@@ -379,12 +434,17 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
379
434
|
})
|
|
380
435
|
|
|
381
436
|
const family = await buildFamilyFromFs(ROOT, dir)
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
)
|
|
387
|
-
expect(
|
|
437
|
+
// U4 语义变化(TC-manifest-source):m0 时 alive 无 identity → 完全丢弃(无数据源建族)。
|
|
438
|
+
// U4 manifest 主路径:manifest 命中即建族(manifest 有 rootSessionId),不再依赖 identity。
|
|
439
|
+
// MF-3 核心保护仍生效:不被收编为 cleanedUp(fileStats 有 realId → cleanedUp=false)。
|
|
440
|
+
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
441
|
+
expect(sub).toBeDefined()
|
|
442
|
+
expect(sub!.cleanedUp).toBe(false) // 核心:运行中不被当孤儿
|
|
443
|
+
expect(sub!.rootSessionId).toBe(ROOT) // manifest 主:rootSessionId 从 manifest 透传
|
|
444
|
+
expect(sub!.agentName).toBe('explorer') // manifest.agentName → agentName
|
|
445
|
+
// 无 identity 尾行 → task/model/status 取决于 manifest(本 fixture manifest 未写这些 → undefined)
|
|
446
|
+
expect(sub!.task).toBeUndefined()
|
|
447
|
+
expect(sub!.model).toBeUndefined()
|
|
388
448
|
expect(family.subagents.every((s) => s.cleanedUp === false)).toBe(true)
|
|
389
449
|
})
|
|
390
450
|
})
|
|
@@ -395,6 +455,9 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
395
455
|
|
|
396
456
|
describe.skipIf(!HAS_REAL_ROOT)('buildFamilyFromFs - 真实数据 ~/.pi/agent', () => {
|
|
397
457
|
it('019fe620 family:fork 019fe632(跨 cwd)+ 隔代 subagent 019fe635(真实 id)', async () => {
|
|
458
|
+
// 数据守卫:019fe632(sessions/)与 019fe635(subagents/)位于活跃数据目录,
|
|
459
|
+
// 被 GC/重命名时跳过而非失败(同 TC14-TC18 守卫模式),避免偶发红。
|
|
460
|
+
if (!hasAnyRealSession('019fe632') || !hasAnyRealSession('019fe635')) return
|
|
398
461
|
const family = await buildFamilyFromFs(
|
|
399
462
|
'019fe620-8ae1-78a7-b76a-43a1ba4cc3c7',
|
|
400
463
|
REAL_AGENT_DIR,
|
|
@@ -413,6 +476,17 @@ describe.skipIf(!HAS_REAL_ROOT)('buildFamilyFromFs - 真实数据 ~/.pi/agent',
|
|
|
413
476
|
|
|
414
477
|
describe.skipIf(!HAS_REAL_WF)('buildFamilyFromFs - 真实 workflow 数据', () => {
|
|
415
478
|
it('019fdcda:workflows 非空,至少一个 workflow calls>=4', async () => {
|
|
479
|
+
// 数据守卫:workflow-state 快照文件可能被清理(wf-state 是会话运行产物),
|
|
480
|
+
// 019fdcda 的 workflow-state-link 指向的快照不存在时跳过而非失败。
|
|
481
|
+
const wfFile = execSync(
|
|
482
|
+
`find ${REAL_AGENT_DIR}/sessions -name '*019fdcda*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
483
|
+
{ encoding: 'utf8' },
|
|
484
|
+
).trim()
|
|
485
|
+
if (!wfFile) return
|
|
486
|
+
const raw = execSync(`grep -c 'workflow-state-link' '${wfFile}' 2>/dev/null || true`, {
|
|
487
|
+
encoding: 'utf8',
|
|
488
|
+
}).trim()
|
|
489
|
+
if (raw === '0' || raw === '') return
|
|
416
490
|
const family = await buildFamilyFromFs(
|
|
417
491
|
'019fdcda-75c7-74b7-a160-f67f6bf88384',
|
|
418
492
|
REAL_AGENT_DIR,
|
|
@@ -428,3 +502,238 @@ describe.skipIf(!HAS_REAL_WF)('buildFamilyFromFs - 真实 workflow 数据', () =
|
|
|
428
502
|
expect(rich!.runId.startsWith('wf-')).toBe(true)
|
|
429
503
|
}, 30000)
|
|
430
504
|
})
|
|
505
|
+
|
|
506
|
+
// ============================================================
|
|
507
|
+
// w2 TC1:listRecordManifests + RecordManifest 导出(IF2/DM2,行为零变更验证)
|
|
508
|
+
// ============================================================
|
|
509
|
+
|
|
510
|
+
describe('listRecordManifests 导出(w2 TC1)', () => {
|
|
511
|
+
let dir: string
|
|
512
|
+
|
|
513
|
+
beforeEach(async () => {
|
|
514
|
+
dir = await makeAgentDir()
|
|
515
|
+
})
|
|
516
|
+
afterEach(async () => {
|
|
517
|
+
await rm(dir, { recursive: true, force: true })
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('导出生效:import + 调用返回 RecordManifest[],字段完整', async () => {
|
|
521
|
+
await writeRecordManifest(dir, '--demo-cwd--', 'sa-tc1', {
|
|
522
|
+
rootSessionId: 'root-1',
|
|
523
|
+
agentName: 'explorer',
|
|
524
|
+
sessionFile: '/tmp/sa-tc1.jsonl',
|
|
525
|
+
})
|
|
526
|
+
const manifests: RecordManifest[] = await listRecordManifests(dir)
|
|
527
|
+
expect(manifests).toHaveLength(1)
|
|
528
|
+
const m = manifests[0]
|
|
529
|
+
expect(m.id).toBe('sa-tc1')
|
|
530
|
+
expect(m.rootSessionId).toBe('root-1')
|
|
531
|
+
expect(m.agentName).toBe('explorer')
|
|
532
|
+
expect(m.sessionFile).toBe('/tmp/sa-tc1.jsonl')
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
// ============================================================
|
|
537
|
+
// U4 端到端:buildFamilyFromFs 富化(manifest 主 / P-fallback identity 回退 / orphan / compat)
|
|
538
|
+
// 验证数据流重组:manifest 索引命中透全字段,未命中读尾行 identity 回退,孤儿 manifest 富字段+cleanedUp
|
|
539
|
+
// ============================================================
|
|
540
|
+
|
|
541
|
+
describe('U4 buildFamilyFromFs 富化(manifest 主 / P-fallback)', () => {
|
|
542
|
+
let dir: string
|
|
543
|
+
|
|
544
|
+
beforeEach(async () => {
|
|
545
|
+
dir = await makeAgentDir()
|
|
546
|
+
})
|
|
547
|
+
afterEach(async () => {
|
|
548
|
+
await rm(dir, { recursive: true, force: true })
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
it('TC-u4-manifest-enrich: alive + manifest 全字段 → SubagentRef 富字段全透传', async () => {
|
|
552
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
553
|
+
const subPath = await writeSubagentSession(dir, '--root-cwd--', SUB_REAL, {
|
|
554
|
+
rootSessionId: ROOT,
|
|
555
|
+
slug: 'identity-slug', // identity.slug,manifest 主时被 manifest.slug 覆盖
|
|
556
|
+
task: 'identity-task', // identity.task,manifest 主时被 manifest.task 覆盖
|
|
557
|
+
agent: 'worker', // identity.agent,manifest 主时被 manifest.agentName 覆盖
|
|
558
|
+
})
|
|
559
|
+
// manifest 全字段(命中 meta.path 索引 → 走 manifest 主路径,覆盖 identity 值)
|
|
560
|
+
await writeRecordManifest(dir, '--root-cwd--', `sa-${SUB_REAL}`, {
|
|
561
|
+
rootSessionId: ROOT,
|
|
562
|
+
agentName: 'explorer',
|
|
563
|
+
sessionFile: subPath,
|
|
564
|
+
task: '调研 codex',
|
|
565
|
+
slug: 'codex-ask-user-research',
|
|
566
|
+
model: 'glm-5.2',
|
|
567
|
+
status: 'completed',
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
571
|
+
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
572
|
+
|
|
573
|
+
expect(sub).toBeDefined()
|
|
574
|
+
// manifest 主:富字段从 manifest 透传(覆盖 identity 的值)
|
|
575
|
+
expect(sub!.task).toBe('调研 codex')
|
|
576
|
+
expect(sub!.slug).toBe('codex-ask-user-research')
|
|
577
|
+
expect(sub!.agentName).toBe('explorer') // manifest.agentName → agentName
|
|
578
|
+
expect(sub!.model).toBe('glm-5.2')
|
|
579
|
+
expect(sub!.status).toBe('completed')
|
|
580
|
+
expect(sub!.sessionFile).toBe(subPath)
|
|
581
|
+
expect(sub!.cleanedUp).toBe(false)
|
|
582
|
+
expect(sub!.rootSessionId).toBe(ROOT)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it('TC-u4-pfallback-identity: alive 无 manifest + identity 含 task/agent → 回退 identity,model/status undefined', async () => {
|
|
586
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
587
|
+
// 只写 alive session(含 identity 尾行),不写 manifest → P-fallback 路径
|
|
588
|
+
const subPath = await writeSubagentSession(dir, '--root-cwd--', SUB_REAL, {
|
|
589
|
+
rootSessionId: ROOT,
|
|
590
|
+
slug: 'fix',
|
|
591
|
+
task: 'fix bug',
|
|
592
|
+
agent: 'worker',
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
596
|
+
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
597
|
+
|
|
598
|
+
expect(sub).toBeDefined()
|
|
599
|
+
// P-fallback:task/agent/sessionFile 从 identity 回退
|
|
600
|
+
expect(sub!.task).toBe('fix bug')
|
|
601
|
+
expect(sub!.slug).toBe('fix')
|
|
602
|
+
expect(sub!.agentName).toBe('worker') // identity.data.agent → agentName
|
|
603
|
+
expect(sub!.sessionFile).toBe(subPath) // P-fallback sessionFile=alive meta.path
|
|
604
|
+
// P-fallback 核心断言:model/status 不可回退,必 undefined
|
|
605
|
+
expect(sub!.model).toBeUndefined()
|
|
606
|
+
expect(sub!.status).toBeUndefined()
|
|
607
|
+
expect(sub!.cleanedUp).toBe(false)
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
it('TC-u4-pfallback-no-identity: alive 无 manifest 无 identity(运行中)→ 不入 family.subagents,不抛错', async () => {
|
|
611
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
612
|
+
// alive 文件无 identity 尾行(运行中/异常),无 manifest
|
|
613
|
+
await writeAliveSubagentNoIdentity(dir, '--root-cwd--', SUB_REAL)
|
|
614
|
+
|
|
615
|
+
// 核心断言:buildFamilyFromFs 不抛错、不崩溃
|
|
616
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
617
|
+
|
|
618
|
+
// 无 manifest 无 identity → 无法确定 rootSessionId → 不入 family.subagents
|
|
619
|
+
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
620
|
+
expect(sub).toBeUndefined()
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
it('TC-u4-orphan-manifest: manifest 全字段 + sessionFile 指向不存在路径 → 孤儿 cleanedUp=true,富字段透传', async () => {
|
|
624
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
625
|
+
const ghostPath = '/nonexistent/gc-ghost.jsonl' // .jsonl 已被 GC(不写该文件)
|
|
626
|
+
await writeRecordManifest(dir, '--root-cwd--', 'sa-ghost-id', {
|
|
627
|
+
rootSessionId: ROOT,
|
|
628
|
+
agentName: 'worker',
|
|
629
|
+
sessionFile: ghostPath,
|
|
630
|
+
task: 'ghost task',
|
|
631
|
+
slug: 'ghost-slug',
|
|
632
|
+
model: 'gpt-4',
|
|
633
|
+
status: 'completed',
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
637
|
+
const ghost = family.subagents.find((s) => s.sessionId === 'sa-ghost-id')
|
|
638
|
+
|
|
639
|
+
expect(ghost).toBeDefined()
|
|
640
|
+
expect(ghost!.cleanedUp).toBe(true) // 文件 GC → cleanedUp
|
|
641
|
+
// 孤儿用 manifest 完整富字段
|
|
642
|
+
expect(ghost!.task).toBe('ghost task')
|
|
643
|
+
expect(ghost!.slug).toBe('ghost-slug')
|
|
644
|
+
expect(ghost!.agentName).toBe('worker')
|
|
645
|
+
expect(ghost!.model).toBe('gpt-4')
|
|
646
|
+
expect(ghost!.status).toBe('completed')
|
|
647
|
+
expect(ghost!.sessionFile).toBe(ghostPath) // GC 路径保留(不置空)
|
|
648
|
+
expect(ghost!.rootSessionId).toBe(ROOT)
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
it('TC-u4-recordmanifest-compat: 旧 manifest(仅 id/rootSessionId/sessionFile)→ 富字段 undefined,不抛错', async () => {
|
|
652
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
653
|
+
// 旧格式 manifest:无 task/slug/model/status/agentName
|
|
654
|
+
await writeRecordManifest(dir, '--root-cwd--', 'sa-old', {
|
|
655
|
+
rootSessionId: ROOT,
|
|
656
|
+
sessionFile: '/nonexistent/old.jsonl',
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
// listRecordManifests 兼容:返回该 manifest,富字段 undefined
|
|
660
|
+
const manifests = await listRecordManifests(dir)
|
|
661
|
+
expect(manifests).toHaveLength(1)
|
|
662
|
+
const m = manifests[0]
|
|
663
|
+
expect(m.id).toBe('sa-old')
|
|
664
|
+
expect(m.rootSessionId).toBe(ROOT)
|
|
665
|
+
expect(m.task).toBeUndefined()
|
|
666
|
+
expect(m.slug).toBeUndefined()
|
|
667
|
+
expect(m.model).toBeUndefined()
|
|
668
|
+
expect(m.status).toBeUndefined()
|
|
669
|
+
expect(m.agentName).toBeUndefined()
|
|
670
|
+
|
|
671
|
+
// buildFamilyFromFs 消费旧 manifest(孤儿路径)不抛错,富字段 undefined
|
|
672
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
673
|
+
const ghost = family.subagents.find((s) => s.sessionId === 'sa-old')
|
|
674
|
+
expect(ghost).toBeDefined()
|
|
675
|
+
expect(ghost!.cleanedUp).toBe(true)
|
|
676
|
+
expect(ghost!.task).toBeUndefined()
|
|
677
|
+
expect(ghost!.model).toBeUndefined()
|
|
678
|
+
// 旧 manifest slug 缺失 → 回退 agentName(也缺)→ 空串兜底
|
|
679
|
+
expect(ghost!.slug).toBe('')
|
|
680
|
+
})
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
// ============================================================
|
|
684
|
+
// U4 C4 契约:extractSessionIdFromFilename 导出(仅加 export,不改逻辑,供 w4 复用)
|
|
685
|
+
// ============================================================
|
|
686
|
+
|
|
687
|
+
describe('U4 extractSessionIdFromFilename 导出(C4 契约)', () => {
|
|
688
|
+
it('导出生效:从 <timestamp>_<sessionId>.jsonl 提取 sessionId', () => {
|
|
689
|
+
expect(
|
|
690
|
+
extractSessionIdFromFilename('2026-08-07T16-49-48-393Z_019fdd21-8169-7a02-8f11-eef6c9ca11cc.jsonl'),
|
|
691
|
+
).toBe('019fdd21-8169-7a02-8f11-eef6c9ca11cc')
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
it('非 uuid 特征文件名 → 空串(行为不变)', () => {
|
|
695
|
+
expect(extractSessionIdFromFilename('not-a-uuid.jsonl')).toBe('')
|
|
696
|
+
expect(extractSessionIdFromFilename('readme.txt')).toBe('')
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it('无下划线的纯 uuid 文件名 → 返回 uuid(兼容简化场景)', () => {
|
|
700
|
+
expect(extractSessionIdFromFilename('019fdd21-8169-7a02-8f11-eef6c9ca11cc.jsonl')).toBe(
|
|
701
|
+
'019fdd21-8169-7a02-8f11-eef6c9ca11cc',
|
|
702
|
+
)
|
|
703
|
+
})
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
// ============================================================
|
|
707
|
+
// U4 真实数据守卫(TC-u4-real-data-guard):~/.pi/agent 富字段从 manifest 正确透传
|
|
708
|
+
// ============================================================
|
|
709
|
+
|
|
710
|
+
const HAS_REAL_SUBAGENTS_DIR = existsSync(join(REAL_AGENT_DIR, 'subagents'))
|
|
711
|
+
|
|
712
|
+
describe.skipIf(!HAS_REAL_ROOT || !HAS_REAL_SUBAGENTS_DIR)('U4 真实数据守卫:~/.pi/agent', () => {
|
|
713
|
+
it('TC-u4-real-data-guard: 019fe620 subagents 富字段透传(manifest 主 task 非空率 > 80%)', async () => {
|
|
714
|
+
// 数据守卫:019fe635 是 019fe620 家族已知的隔代 subagent(含 task 富字段的锚点),
|
|
715
|
+
// 被 GC 后跳过富字段验证(manifest 数据不可复现,跳过比失败合理)。
|
|
716
|
+
if (!hasAnyRealSession('019fe635')) return
|
|
717
|
+
const family = await buildFamilyFromFs(
|
|
718
|
+
'019fe620-8ae1-78a7-b76a-43a1ba4cc3c7',
|
|
719
|
+
REAL_AGENT_DIR,
|
|
720
|
+
)
|
|
721
|
+
// 有 subagent 才验证富字段(019fe620 确有隔代 subagent,见既有真实数据测试)
|
|
722
|
+
expect(family.subagents.length).toBeGreaterThan(0)
|
|
723
|
+
|
|
724
|
+
// manifest 主的 subagent(model !== undefined 近似判定,探针 manifest 20/20 有 model):
|
|
725
|
+
// task 非空率应 > 80%(探针 20/20 manifest 全有 task)
|
|
726
|
+
const manifestSourced = family.subagents.filter((s) => s.model !== undefined)
|
|
727
|
+
if (manifestSourced.length > 0) {
|
|
728
|
+
const withTask = manifestSourced.filter((s) => s.task !== undefined && s.task !== '')
|
|
729
|
+
expect(withTask.length / manifestSourced.length).toBeGreaterThan(0.8)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// P-fallback 的 subagent(model === undefined):status 必 undefined(identity 不可回退 status)
|
|
733
|
+
const pfallback = family.subagents.filter((s) => s.model === undefined)
|
|
734
|
+
expect(pfallback.every((s) => s.status === undefined)).toBe(true)
|
|
735
|
+
|
|
736
|
+
// 所有 subagent 的 sessionFile 非空(manifest.sessionFile 或 alive meta.path)
|
|
737
|
+
expect(family.subagents.every((s) => typeof s.sessionFile === 'string' && s.sessionFile.length > 0)).toBe(true)
|
|
738
|
+
}, 30000)
|
|
739
|
+
})
|