@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
|
@@ -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'
|
|
@@ -54,12 +60,13 @@ async function writeMainSession(
|
|
|
54
60
|
/**
|
|
55
61
|
* 写 subagent session 文件:首行 header(真实 id)+ 占位 message + 尾行 identity。
|
|
56
62
|
* identity 在尾行(实测 pi 行为;subagent-identity 由 session-runner 在 session 创建后写)。
|
|
63
|
+
* U4 扩展:identity.task/agent 可定制(P-fallback 测试用,默认 't'/'explorer')。
|
|
57
64
|
*/
|
|
58
65
|
async function writeSubagentSession(
|
|
59
66
|
dir: string,
|
|
60
67
|
slug: string,
|
|
61
|
-
|
|
62
|
-
identity: { rootSessionId: string; slug: string; dataId?: string },
|
|
68
|
+
realId: string,
|
|
69
|
+
identity: { rootSessionId: string; slug: string; dataId?: string; task?: string; agent?: string },
|
|
63
70
|
): Promise<string> {
|
|
64
71
|
const sessionDir = join(dir, 'subagents', slug, 'sessions')
|
|
65
72
|
await mkdir(sessionDir, { recursive: true })
|
|
@@ -79,9 +86,9 @@ async function writeSubagentSession(
|
|
|
79
86
|
id: identity.dataId ?? `sa-${realId.slice(0, 8)}`,
|
|
80
87
|
rootSessionId: identity.rootSessionId,
|
|
81
88
|
slug: identity.slug,
|
|
82
|
-
agent: 'explorer',
|
|
89
|
+
agent: identity.agent ?? 'explorer',
|
|
83
90
|
mode: 'sync',
|
|
84
|
-
task: 't',
|
|
91
|
+
task: identity.task ?? 't',
|
|
85
92
|
startedAt: 1,
|
|
86
93
|
},
|
|
87
94
|
}),
|
|
@@ -90,6 +97,31 @@ async function writeSubagentSession(
|
|
|
90
97
|
return path
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
/**
|
|
101
|
+
* 写 alive subagent 文件但**无 identity 尾行**(运行中/异常,尾行是 message)。
|
|
102
|
+
* TC-u4-pfallback-no-identity 场景:header 有效、无 manifest、无 identity → buildFamilyFromFs 跳过。
|
|
103
|
+
*/
|
|
104
|
+
async function writeAliveSubagentNoIdentity(
|
|
105
|
+
dir: string,
|
|
106
|
+
slug: string,
|
|
107
|
+
realId: string,
|
|
108
|
+
): Promise<string> {
|
|
109
|
+
const sessionDir = join(dir, 'subagents', slug, 'sessions')
|
|
110
|
+
await mkdir(sessionDir, { recursive: true })
|
|
111
|
+
const path = join(sessionDir, `${realId}.jsonl`)
|
|
112
|
+
const lines = [
|
|
113
|
+
JSON.stringify({ type: 'session', id: realId, cwd: `/proj/${slug}` }),
|
|
114
|
+
JSON.stringify({
|
|
115
|
+
type: 'message',
|
|
116
|
+
id: 'm1',
|
|
117
|
+
parentId: realId,
|
|
118
|
+
message: { role: 'user', content: 'still running' },
|
|
119
|
+
}),
|
|
120
|
+
]
|
|
121
|
+
await writeFile(path, lines.join('\n') + '\n')
|
|
122
|
+
return path
|
|
123
|
+
}
|
|
124
|
+
|
|
93
125
|
/** 写 wf-state 文件(每行一个快照;字符串按原样写入,可注入坏行)。返回绝对路径。 */
|
|
94
126
|
async function writeWfState(dir: string, slug: string, lines: string[]): Promise<string> {
|
|
95
127
|
const wfDir = join(dir, 'sessions', slug, 'workflow-state')
|
|
@@ -118,12 +150,20 @@ async function writeWfLink(
|
|
|
118
150
|
await writeFile(sessionPath, line + '\n', { flag: 'a' })
|
|
119
151
|
}
|
|
120
152
|
|
|
121
|
-
/** 写 records manifest(孤儿源)。 */
|
|
153
|
+
/** 写 records manifest(孤儿源)。U4 扩展:fields 支持富字段 task/slug/model/status。 */
|
|
122
154
|
async function writeRecordManifest(
|
|
123
155
|
dir: string,
|
|
124
156
|
slug: string,
|
|
125
157
|
id: string,
|
|
126
|
-
fields: {
|
|
158
|
+
fields: {
|
|
159
|
+
rootSessionId: string
|
|
160
|
+
agentName?: string
|
|
161
|
+
sessionFile: string
|
|
162
|
+
task?: string
|
|
163
|
+
slug?: string
|
|
164
|
+
model?: string
|
|
165
|
+
status?: string
|
|
166
|
+
},
|
|
127
167
|
): Promise<void> {
|
|
128
168
|
const recordsDir = join(dir, 'subagents', slug, 'records')
|
|
129
169
|
await mkdir(recordsDir, { recursive: true })
|
|
@@ -362,7 +402,7 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
362
402
|
expect(family.workflows[0].calls[0].mtime).toBe(0)
|
|
363
403
|
})
|
|
364
404
|
|
|
365
|
-
it('MF-3 回归:alive 但无 identity 的 subagent
|
|
405
|
+
it('MF-3 回归:alive 但无 identity 的 subagent(运行中)不被收编为 cleanedUp——U4 后 manifest 主路径建族', async () => {
|
|
366
406
|
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
367
407
|
// 运行中 subagent:有效 header、无 identity 尾行(identity 完成时才写入),manifest 已存在
|
|
368
408
|
const subDir = join(dir, 'subagents', '--root-cwd--', 'sessions')
|
|
@@ -379,12 +419,17 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
379
419
|
})
|
|
380
420
|
|
|
381
421
|
const family = await buildFamilyFromFs(ROOT, dir)
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
)
|
|
387
|
-
expect(
|
|
422
|
+
// U4 语义变化(TC-manifest-source):m0 时 alive 无 identity → 完全丢弃(无数据源建族)。
|
|
423
|
+
// U4 manifest 主路径:manifest 命中即建族(manifest 有 rootSessionId),不再依赖 identity。
|
|
424
|
+
// MF-3 核心保护仍生效:不被收编为 cleanedUp(fileStats 有 realId → cleanedUp=false)。
|
|
425
|
+
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
426
|
+
expect(sub).toBeDefined()
|
|
427
|
+
expect(sub!.cleanedUp).toBe(false) // 核心:运行中不被当孤儿
|
|
428
|
+
expect(sub!.rootSessionId).toBe(ROOT) // manifest 主:rootSessionId 从 manifest 透传
|
|
429
|
+
expect(sub!.agentName).toBe('explorer') // manifest.agentName → agentName
|
|
430
|
+
// 无 identity 尾行 → task/model/status 取决于 manifest(本 fixture manifest 未写这些 → undefined)
|
|
431
|
+
expect(sub!.task).toBeUndefined()
|
|
432
|
+
expect(sub!.model).toBeUndefined()
|
|
388
433
|
expect(family.subagents.every((s) => s.cleanedUp === false)).toBe(true)
|
|
389
434
|
})
|
|
390
435
|
})
|
|
@@ -428,3 +473,235 @@ describe.skipIf(!HAS_REAL_WF)('buildFamilyFromFs - 真实 workflow 数据', () =
|
|
|
428
473
|
expect(rich!.runId.startsWith('wf-')).toBe(true)
|
|
429
474
|
}, 30000)
|
|
430
475
|
})
|
|
476
|
+
|
|
477
|
+
// ============================================================
|
|
478
|
+
// w2 TC1:listRecordManifests + RecordManifest 导出(IF2/DM2,行为零变更验证)
|
|
479
|
+
// ============================================================
|
|
480
|
+
|
|
481
|
+
describe('listRecordManifests 导出(w2 TC1)', () => {
|
|
482
|
+
let dir: string
|
|
483
|
+
|
|
484
|
+
beforeEach(async () => {
|
|
485
|
+
dir = await makeAgentDir()
|
|
486
|
+
})
|
|
487
|
+
afterEach(async () => {
|
|
488
|
+
await rm(dir, { recursive: true, force: true })
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('导出生效:import + 调用返回 RecordManifest[],字段完整', async () => {
|
|
492
|
+
await writeRecordManifest(dir, '--demo-cwd--', 'sa-tc1', {
|
|
493
|
+
rootSessionId: 'root-1',
|
|
494
|
+
agentName: 'explorer',
|
|
495
|
+
sessionFile: '/tmp/sa-tc1.jsonl',
|
|
496
|
+
})
|
|
497
|
+
const manifests: RecordManifest[] = await listRecordManifests(dir)
|
|
498
|
+
expect(manifests).toHaveLength(1)
|
|
499
|
+
const m = manifests[0]
|
|
500
|
+
expect(m.id).toBe('sa-tc1')
|
|
501
|
+
expect(m.rootSessionId).toBe('root-1')
|
|
502
|
+
expect(m.agentName).toBe('explorer')
|
|
503
|
+
expect(m.sessionFile).toBe('/tmp/sa-tc1.jsonl')
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
// ============================================================
|
|
508
|
+
// U4 端到端:buildFamilyFromFs 富化(manifest 主 / P-fallback identity 回退 / orphan / compat)
|
|
509
|
+
// 验证数据流重组:manifest 索引命中透全字段,未命中读尾行 identity 回退,孤儿 manifest 富字段+cleanedUp
|
|
510
|
+
// ============================================================
|
|
511
|
+
|
|
512
|
+
describe('U4 buildFamilyFromFs 富化(manifest 主 / P-fallback)', () => {
|
|
513
|
+
let dir: string
|
|
514
|
+
|
|
515
|
+
beforeEach(async () => {
|
|
516
|
+
dir = await makeAgentDir()
|
|
517
|
+
})
|
|
518
|
+
afterEach(async () => {
|
|
519
|
+
await rm(dir, { recursive: true, force: true })
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
it('TC-u4-manifest-enrich: alive + manifest 全字段 → SubagentRef 富字段全透传', async () => {
|
|
523
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
524
|
+
const subPath = await writeSubagentSession(dir, '--root-cwd--', SUB_REAL, {
|
|
525
|
+
rootSessionId: ROOT,
|
|
526
|
+
slug: 'identity-slug', // identity.slug,manifest 主时被 manifest.slug 覆盖
|
|
527
|
+
task: 'identity-task', // identity.task,manifest 主时被 manifest.task 覆盖
|
|
528
|
+
agent: 'worker', // identity.agent,manifest 主时被 manifest.agentName 覆盖
|
|
529
|
+
})
|
|
530
|
+
// manifest 全字段(命中 meta.path 索引 → 走 manifest 主路径,覆盖 identity 值)
|
|
531
|
+
await writeRecordManifest(dir, '--root-cwd--', `sa-${SUB_REAL}`, {
|
|
532
|
+
rootSessionId: ROOT,
|
|
533
|
+
agentName: 'explorer',
|
|
534
|
+
sessionFile: subPath,
|
|
535
|
+
task: '调研 codex',
|
|
536
|
+
slug: 'codex-ask-user-research',
|
|
537
|
+
model: 'glm-5.2',
|
|
538
|
+
status: 'completed',
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
542
|
+
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
543
|
+
|
|
544
|
+
expect(sub).toBeDefined()
|
|
545
|
+
// manifest 主:富字段从 manifest 透传(覆盖 identity 的值)
|
|
546
|
+
expect(sub!.task).toBe('调研 codex')
|
|
547
|
+
expect(sub!.slug).toBe('codex-ask-user-research')
|
|
548
|
+
expect(sub!.agentName).toBe('explorer') // manifest.agentName → agentName
|
|
549
|
+
expect(sub!.model).toBe('glm-5.2')
|
|
550
|
+
expect(sub!.status).toBe('completed')
|
|
551
|
+
expect(sub!.sessionFile).toBe(subPath)
|
|
552
|
+
expect(sub!.cleanedUp).toBe(false)
|
|
553
|
+
expect(sub!.rootSessionId).toBe(ROOT)
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
it('TC-u4-pfallback-identity: alive 无 manifest + identity 含 task/agent → 回退 identity,model/status undefined', async () => {
|
|
557
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
558
|
+
// 只写 alive session(含 identity 尾行),不写 manifest → P-fallback 路径
|
|
559
|
+
const subPath = await writeSubagentSession(dir, '--root-cwd--', SUB_REAL, {
|
|
560
|
+
rootSessionId: ROOT,
|
|
561
|
+
slug: 'fix',
|
|
562
|
+
task: 'fix bug',
|
|
563
|
+
agent: 'worker',
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
567
|
+
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
568
|
+
|
|
569
|
+
expect(sub).toBeDefined()
|
|
570
|
+
// P-fallback:task/agent/sessionFile 从 identity 回退
|
|
571
|
+
expect(sub!.task).toBe('fix bug')
|
|
572
|
+
expect(sub!.slug).toBe('fix')
|
|
573
|
+
expect(sub!.agentName).toBe('worker') // identity.data.agent → agentName
|
|
574
|
+
expect(sub!.sessionFile).toBe(subPath) // P-fallback sessionFile=alive meta.path
|
|
575
|
+
// P-fallback 核心断言:model/status 不可回退,必 undefined
|
|
576
|
+
expect(sub!.model).toBeUndefined()
|
|
577
|
+
expect(sub!.status).toBeUndefined()
|
|
578
|
+
expect(sub!.cleanedUp).toBe(false)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('TC-u4-pfallback-no-identity: alive 无 manifest 无 identity(运行中)→ 不入 family.subagents,不抛错', async () => {
|
|
582
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
583
|
+
// alive 文件无 identity 尾行(运行中/异常),无 manifest
|
|
584
|
+
await writeAliveSubagentNoIdentity(dir, '--root-cwd--', SUB_REAL)
|
|
585
|
+
|
|
586
|
+
// 核心断言:buildFamilyFromFs 不抛错、不崩溃
|
|
587
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
588
|
+
|
|
589
|
+
// 无 manifest 无 identity → 无法确定 rootSessionId → 不入 family.subagents
|
|
590
|
+
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
591
|
+
expect(sub).toBeUndefined()
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it('TC-u4-orphan-manifest: manifest 全字段 + sessionFile 指向不存在路径 → 孤儿 cleanedUp=true,富字段透传', async () => {
|
|
595
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
596
|
+
const ghostPath = '/nonexistent/gc-ghost.jsonl' // .jsonl 已被 GC(不写该文件)
|
|
597
|
+
await writeRecordManifest(dir, '--root-cwd--', 'sa-ghost-id', {
|
|
598
|
+
rootSessionId: ROOT,
|
|
599
|
+
agentName: 'worker',
|
|
600
|
+
sessionFile: ghostPath,
|
|
601
|
+
task: 'ghost task',
|
|
602
|
+
slug: 'ghost-slug',
|
|
603
|
+
model: 'gpt-4',
|
|
604
|
+
status: 'completed',
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
608
|
+
const ghost = family.subagents.find((s) => s.sessionId === 'sa-ghost-id')
|
|
609
|
+
|
|
610
|
+
expect(ghost).toBeDefined()
|
|
611
|
+
expect(ghost!.cleanedUp).toBe(true) // 文件 GC → cleanedUp
|
|
612
|
+
// 孤儿用 manifest 完整富字段
|
|
613
|
+
expect(ghost!.task).toBe('ghost task')
|
|
614
|
+
expect(ghost!.slug).toBe('ghost-slug')
|
|
615
|
+
expect(ghost!.agentName).toBe('worker')
|
|
616
|
+
expect(ghost!.model).toBe('gpt-4')
|
|
617
|
+
expect(ghost!.status).toBe('completed')
|
|
618
|
+
expect(ghost!.sessionFile).toBe(ghostPath) // GC 路径保留(不置空)
|
|
619
|
+
expect(ghost!.rootSessionId).toBe(ROOT)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('TC-u4-recordmanifest-compat: 旧 manifest(仅 id/rootSessionId/sessionFile)→ 富字段 undefined,不抛错', async () => {
|
|
623
|
+
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
624
|
+
// 旧格式 manifest:无 task/slug/model/status/agentName
|
|
625
|
+
await writeRecordManifest(dir, '--root-cwd--', 'sa-old', {
|
|
626
|
+
rootSessionId: ROOT,
|
|
627
|
+
sessionFile: '/nonexistent/old.jsonl',
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
// listRecordManifests 兼容:返回该 manifest,富字段 undefined
|
|
631
|
+
const manifests = await listRecordManifests(dir)
|
|
632
|
+
expect(manifests).toHaveLength(1)
|
|
633
|
+
const m = manifests[0]
|
|
634
|
+
expect(m.id).toBe('sa-old')
|
|
635
|
+
expect(m.rootSessionId).toBe(ROOT)
|
|
636
|
+
expect(m.task).toBeUndefined()
|
|
637
|
+
expect(m.slug).toBeUndefined()
|
|
638
|
+
expect(m.model).toBeUndefined()
|
|
639
|
+
expect(m.status).toBeUndefined()
|
|
640
|
+
expect(m.agentName).toBeUndefined()
|
|
641
|
+
|
|
642
|
+
// buildFamilyFromFs 消费旧 manifest(孤儿路径)不抛错,富字段 undefined
|
|
643
|
+
const family = await buildFamilyFromFs(ROOT, dir)
|
|
644
|
+
const ghost = family.subagents.find((s) => s.sessionId === 'sa-old')
|
|
645
|
+
expect(ghost).toBeDefined()
|
|
646
|
+
expect(ghost!.cleanedUp).toBe(true)
|
|
647
|
+
expect(ghost!.task).toBeUndefined()
|
|
648
|
+
expect(ghost!.model).toBeUndefined()
|
|
649
|
+
// 旧 manifest slug 缺失 → 回退 agentName(也缺)→ 空串兜底
|
|
650
|
+
expect(ghost!.slug).toBe('')
|
|
651
|
+
})
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
// ============================================================
|
|
655
|
+
// U4 C4 契约:extractSessionIdFromFilename 导出(仅加 export,不改逻辑,供 w4 复用)
|
|
656
|
+
// ============================================================
|
|
657
|
+
|
|
658
|
+
describe('U4 extractSessionIdFromFilename 导出(C4 契约)', () => {
|
|
659
|
+
it('导出生效:从 <timestamp>_<sessionId>.jsonl 提取 sessionId', () => {
|
|
660
|
+
expect(
|
|
661
|
+
extractSessionIdFromFilename('2026-08-07T16-49-48-393Z_019fdd21-8169-7a02-8f11-eef6c9ca11cc.jsonl'),
|
|
662
|
+
).toBe('019fdd21-8169-7a02-8f11-eef6c9ca11cc')
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
it('非 uuid 特征文件名 → 空串(行为不变)', () => {
|
|
666
|
+
expect(extractSessionIdFromFilename('not-a-uuid.jsonl')).toBe('')
|
|
667
|
+
expect(extractSessionIdFromFilename('readme.txt')).toBe('')
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('无下划线的纯 uuid 文件名 → 返回 uuid(兼容简化场景)', () => {
|
|
671
|
+
expect(extractSessionIdFromFilename('019fdd21-8169-7a02-8f11-eef6c9ca11cc.jsonl')).toBe(
|
|
672
|
+
'019fdd21-8169-7a02-8f11-eef6c9ca11cc',
|
|
673
|
+
)
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
// ============================================================
|
|
678
|
+
// U4 真实数据守卫(TC-u4-real-data-guard):~/.pi/agent 富字段从 manifest 正确透传
|
|
679
|
+
// ============================================================
|
|
680
|
+
|
|
681
|
+
const HAS_REAL_SUBAGENTS_DIR = existsSync(join(REAL_AGENT_DIR, 'subagents'))
|
|
682
|
+
|
|
683
|
+
describe.skipIf(!HAS_REAL_ROOT || !HAS_REAL_SUBAGENTS_DIR)('U4 真实数据守卫:~/.pi/agent', () => {
|
|
684
|
+
it('TC-u4-real-data-guard: 019fe620 subagents 富字段透传(manifest 主 task 非空率 > 80%)', async () => {
|
|
685
|
+
const family = await buildFamilyFromFs(
|
|
686
|
+
'019fe620-8ae1-78a7-b76a-43a1ba4cc3c7',
|
|
687
|
+
REAL_AGENT_DIR,
|
|
688
|
+
)
|
|
689
|
+
// 有 subagent 才验证富字段(019fe620 确有隔代 subagent,见既有真实数据测试)
|
|
690
|
+
expect(family.subagents.length).toBeGreaterThan(0)
|
|
691
|
+
|
|
692
|
+
// manifest 主的 subagent(model !== undefined 近似判定,探针 manifest 20/20 有 model):
|
|
693
|
+
// task 非空率应 > 80%(探针 20/20 manifest 全有 task)
|
|
694
|
+
const manifestSourced = family.subagents.filter((s) => s.model !== undefined)
|
|
695
|
+
if (manifestSourced.length > 0) {
|
|
696
|
+
const withTask = manifestSourced.filter((s) => s.task !== undefined && s.task !== '')
|
|
697
|
+
expect(withTask.length / manifestSourced.length).toBeGreaterThan(0.8)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// P-fallback 的 subagent(model === undefined):status 必 undefined(identity 不可回退 status)
|
|
701
|
+
const pfallback = family.subagents.filter((s) => s.model === undefined)
|
|
702
|
+
expect(pfallback.every((s) => s.status === undefined)).toBe(true)
|
|
703
|
+
|
|
704
|
+
// 所有 subagent 的 sessionFile 非空(manifest.sessionFile 或 alive meta.path)
|
|
705
|
+
expect(family.subagents.every((s) => typeof s.sessionFile === 'string' && s.sessionFile.length > 0)).toBe(true)
|
|
706
|
+
}, 30000)
|
|
707
|
+
})
|