@zhushanwen/pi-session-reader 0.3.1 → 0.5.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.
@@ -1,9 +1,26 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
2
  import { tmpdir } from 'node:os'
3
- import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
4
- import { join } from 'node:path'
5
- import { listMainSessions, listSubagentSessions } from '../discovery/roots.js'
6
- import { REAL_AGENT_DIR, HAS_E6, HAS_REAL_SUBAGENTS_DIR } from './real-data.js'
3
+ import { mkdtemp, mkdir, readdir, writeFile, rm } from 'node:fs/promises'
4
+ import { join, dirname } from 'node:path'
5
+ import {
6
+ listMainSessions,
7
+ listSubagentSessions,
8
+ resolveSessionRoots,
9
+ normalizeLiveSessionDir,
10
+ type SessionRoot,
11
+ type SessionRootSignals,
12
+ } from '../discovery/roots.js'
13
+ import { buildFamilyFromFs } from '../discovery/subagents.js'
14
+ import { REAL_AGENT_DIR, HAS_E6, HAS_REAL_AGENT_DIR, HAS_REAL_SUBAGENTS_DIR, REAL_DATA_TIMEOUT_MS } from './real-data.js'
15
+
16
+ /** 写 .jsonl fixture(父目录不存在则自建),内容默认合法 session header */
17
+ async function writeJsonl(path: string, content = '{"type":"session","id":"x"}\n') {
18
+ await mkdir(dirname(path), { recursive: true })
19
+ await writeFile(path, content)
20
+ }
21
+
22
+ /** 真实形态的 cwd 编码目录名(encodeCwd:`--` 开头 `--` 结尾,实证见 real-data.ts) */
23
+ const SLUG = '--Users-foo--'
7
24
 
8
25
  describe('listMainSessions', () => {
9
26
  let dir: string
@@ -12,7 +29,7 @@ describe('listMainSessions', () => {
12
29
  dir = await mkdtemp(join(tmpdir(), 'roots-test-'))
13
30
  })
14
31
  afterEach(async () => {
15
- await rm(dir, { recursive: true, force: true })
32
+ await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
16
33
  })
17
34
 
18
35
  it('扫描 sessions/<slug>/*.jsonl,排除 *.jsonl.finalized', async () => {
@@ -80,7 +97,7 @@ describe('listMainSessions', () => {
80
97
  expect(result.every((m) => !m.path.split('/').pop()!.startsWith('wf-'))).toBe(true)
81
98
  // mtime/size 真实
82
99
  expect(result.every((m) => m.mtime > 0 && m.size > 0)).toBe(true)
83
- }, 30000)
100
+ }, REAL_DATA_TIMEOUT_MS)
84
101
  })
85
102
 
86
103
  describe('listSubagentSessions', () => {
@@ -90,7 +107,7 @@ describe('listSubagentSessions', () => {
90
107
  dir = await mkdtemp(join(tmpdir(), 'roots-sub-test-'))
91
108
  })
92
109
  afterEach(async () => {
93
- await rm(dir, { recursive: true, force: true })
110
+ await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
94
111
  })
95
112
 
96
113
  it('扫描 subagents/<slug>/sessions/*.jsonl,排除 .finalized', async () => {
@@ -128,5 +145,341 @@ describe('listSubagentSessions', () => {
128
145
  const result = await listSubagentSessions(REAL_AGENT_DIR)
129
146
  expect(result.length).toBeGreaterThan(0)
130
147
  expect(result.every((m) => !m.path.endsWith('.finalized'))).toBe(true)
131
- }, 30000)
148
+ }, REAL_DATA_TIMEOUT_MS)
149
+ })
150
+
151
+ // ============================================================
152
+ // U1 resolveSessionRoots(design 2026-09-10 §6.1 / §7B 要点 2/3/4/6/7;B 收缩无 [env])
153
+ // ============================================================
154
+
155
+ /** 三宿主信号包用例的行内校验上下文 */
156
+ interface HostCaseCtx {
157
+ tmp: string
158
+ agentDir: string
159
+ }
160
+
161
+ interface HostCase {
162
+ name: string
163
+ build: (tmp: string) => Promise<{ agentDir: string; signals: SessionRootSignals }>
164
+ verify: (ctx: HostCaseCtx, roots: SessionRoot[]) => void
165
+ }
166
+
167
+ const HOST_CASES: HostCase[] = [
168
+ {
169
+ name: '纯 pi:live=encodeCwd 子目录,剥层后与 default 同路径去重,保留 live 标签',
170
+ async build(tmp) {
171
+ const agentDir = join(tmp, 'agent')
172
+ await writeJsonl(join(agentDir, 'sessions', SLUG, 'a.jsonl'))
173
+ return {
174
+ agentDir,
175
+ signals: { agentDir, liveSessionDir: join(agentDir, 'sessions', SLUG) },
176
+ }
177
+ },
178
+ verify({ agentDir }, roots) {
179
+ // kind 序 = 优先级序(live > default > legacy > subagent;B 收缩无 env)
180
+ expect(roots.map((r) => r.kind)).toEqual(['live', 'default', 'legacy', 'subagent'])
181
+ const live = roots.find((r) => r.kind === 'live')!
182
+ // 剥层(§7B 要点 3):父目录名为 sessions 的真实形态 → dirname
183
+ expect(live.path).toBe(join(agentDir, 'sessions'))
184
+ expect(live.exists).toBe(true)
185
+ expect(live.fileCount).toBe(1)
186
+ const def = roots.find((r) => r.kind === 'default')!
187
+ expect(def.path).toBe(join(agentDir, 'sessions'))
188
+ // realpath 去重(§7B 要点 4):同路径只扫一次,保留最高优先级 kind
189
+ expect(def.dedupedInto).toBe('live')
190
+ expect(def.files).toHaveLength(0)
191
+ expect(def.fileCount).toBeUndefined()
192
+ // 该路径在全列表中只被一个根实扫(文件不重复计入)
193
+ const scannedAtSamePath = roots.filter(
194
+ (r) => r.path === join(agentDir, 'sessions') && r.dedupedInto === undefined,
195
+ )
196
+ expect(scannedAtSamePath).toHaveLength(1)
197
+ expect(roots.reduce((n, r) => n + r.files.length, 0)).toBe(1)
198
+ },
199
+ },
200
+ {
201
+ name: 'xyz-agent:live=根本身(RPC 覆盖态),default 去重,legacy 残留与 subagent 独立列出',
202
+ async build(tmp) {
203
+ const dataDir = join(tmp, 'data')
204
+ const agentDir = join(dataDir, 'agent')
205
+ await writeJsonl(join(agentDir, 'sessions', SLUG, 'x.jsonl'))
206
+ await writeJsonl(join(dataDir, 'sessions', 'old.jsonl')) // 旧旧布局迁移残留(§6.13 U1 行)
207
+ await writeJsonl(join(agentDir, 'subagents', SLUG, 'sessions', 's.jsonl'))
208
+ return { agentDir, signals: { agentDir, liveSessionDir: join(agentDir, 'sessions') } }
209
+ },
210
+ verify({ agentDir, tmp }, roots) {
211
+ expect(roots.map((r) => r.kind)).toEqual(['live', 'default', 'legacy', 'subagent'])
212
+ // live=根本身(basename 非 encodeCwd 形态)→ 取自身,与 default 同字面路径 → 去重
213
+ const scanned = roots.filter((r) => r.dedupedInto === undefined)
214
+ expect(scanned.map((r) => r.kind)).toEqual(['live', 'legacy', 'subagent'])
215
+ const live = scanned[0]!
216
+ expect(live.path).toBe(join(agentDir, 'sessions'))
217
+ expect(live.fileCount).toBe(1) // 递归扫到 encodeCwd 子目录内的文件
218
+ const def = roots.find((r) => r.kind === 'default')!
219
+ expect(def.dedupedInto).toBe('live')
220
+ // legacy(<dataDir>/sessions 残留)非空即纳入候选,独立成根
221
+ const legacy = roots.find((r) => r.kind === 'legacy')!
222
+ expect(legacy.path).toBe(join(tmp, 'data', 'sessions'))
223
+ expect(legacy.exists).toBe(true)
224
+ expect(legacy.fileCount).toBe(1)
225
+ const sub = roots.find((r) => r.kind === 'subagent')!
226
+ expect(sub.exists).toBe(true)
227
+ expect(sub.fileCount).toBe(1)
228
+ expect(sub.files[0]!.path.endsWith('s.jsonl')).toBe(true)
229
+ },
230
+ },
231
+ {
232
+ name: 'liveSessionDir 缺失降级:default/legacy/subagent 三根仍可用,无 live 根',
233
+ async build(tmp) {
234
+ const agentDir = join(tmp, 'agent')
235
+ await writeJsonl(join(agentDir, 'sessions', SLUG, 'a.jsonl'))
236
+ return { agentDir, signals: { agentDir } }
237
+ },
238
+ verify({ agentDir, tmp }, roots) {
239
+ expect(roots.map((r) => r.kind)).toEqual(['default', 'legacy', 'subagent'])
240
+ const def = roots.find((r) => r.kind === 'default')!
241
+ expect(def.path).toBe(join(agentDir, 'sessions'))
242
+ expect(def.exists).toBe(true)
243
+ expect(def.fileCount).toBe(1)
244
+ expect(roots.find((r) => r.kind === 'legacy')!.path).toBe(join(tmp, 'sessions'))
245
+ // live 缺席 → 无去重发生
246
+ expect(roots.every((r) => r.dedupedInto === undefined)).toBe(true)
247
+ },
248
+ },
249
+ ]
250
+
251
+ describe('resolveSessionRoots', () => {
252
+ it.each(HOST_CASES)('$name', async ({ build, verify }) => {
253
+ const tmp = await mkdtemp(join(tmpdir(), 'roots-resolve-'))
254
+ try {
255
+ const { agentDir, signals } = await build(tmp)
256
+ const roots = await resolveSessionRoots(signals)
257
+
258
+ // ④ subagent 根恒在(常量推导,不来自任何信号,§6.1 信号 5)
259
+ const sub = roots.find((r) => r.kind === 'subagent')
260
+ expect(sub).toBeDefined()
261
+ expect(sub!.source).toBe('subagent')
262
+ expect(sub!.path).toBe(join(agentDir, 'subagents'))
263
+
264
+ // ⑤ 已扫描根 fileCount/scanMs 恒填充且与 files 一致;被去重根不产生扫描统计
265
+ for (const r of roots) {
266
+ if (r.dedupedInto !== undefined) {
267
+ expect(r.fileCount).toBeUndefined()
268
+ expect(r.scanMs).toBeUndefined()
269
+ } else {
270
+ expect(typeof r.fileCount).toBe('number')
271
+ expect(r.fileCount).toBe(r.files.length)
272
+ expect(typeof r.scanMs).toBe('number')
273
+ expect(r.scanMs!).toBeGreaterThanOrEqual(0)
274
+ }
275
+ }
276
+
277
+ verify({ tmp, agentDir }, roots)
278
+ } finally {
279
+ await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
280
+ }
281
+ })
282
+
283
+ it('[live] 规范化第二形态:普通目录(basename 非 encodeCwd 形态)取自身,不剥层', async () => {
284
+ const tmp = await mkdtemp(join(tmpdir(), 'roots-live-plain-'))
285
+ try {
286
+ const agentDir = join(tmp, 'agent')
287
+ const liveDir = join(tmp, 'live-sessions')
288
+ await writeJsonl(join(liveDir, 'b.jsonl'))
289
+
290
+ const roots = await resolveSessionRoots({ agentDir, liveSessionDir: liveDir })
291
+ const live = roots.find((r) => r.kind === 'live')!
292
+ expect(live.path).toBe(liveDir)
293
+ expect(live.exists).toBe(true)
294
+ expect(live.fileCount).toBe(1)
295
+ // default/legacy 独立列出,互不干扰
296
+ expect(roots.find((r) => r.kind === 'default')!.path).toBe(join(agentDir, 'sessions'))
297
+ expect(roots.every((r) => r.dedupedInto === undefined)).toBe(true)
298
+ } finally {
299
+ await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
300
+ }
301
+ })
302
+
303
+ it('空 agentDir 防御:不派生任何根(避免扫到进程 cwd 相对路径)', async () => {
304
+ await expect(resolveSessionRoots({ agentDir: '' })).resolves.toEqual([])
305
+ })
306
+
307
+ it.skipIf(!HAS_REAL_AGENT_DIR)(
308
+ '§11.2 真实路径集合回归:encodeCwd 判据在真实 sessions 子目录上自洽,剥层分支被真实数据覆盖',
309
+ async () => {
310
+ const sessionsDir = join(REAL_AGENT_DIR, 'sessions')
311
+ const entries = await readdir(sessionsDir, { withFileTypes: true })
312
+ const dirs = entries.filter((e) => e.isDirectory())
313
+ expect(dirs.length).toBeGreaterThan(0)
314
+ let encodeCwdCount = 0
315
+ for (const d of dirs) {
316
+ const asLive = join(sessionsDir, d.name)
317
+ const matchesShape = d.name.startsWith('--') && d.name.endsWith('--')
318
+ // 判据自洽:匹配形态 → 剥层到父目录;不匹配(如 permission-forwarding/)→ 取自身
319
+ expect(normalizeLiveSessionDir(asLive)).toBe(matchesShape ? sessionsDir : asLive)
320
+ if (matchesShape) encodeCwdCount++
321
+ }
322
+ // 真实集合以 encodeCwd 目录为主体——剥层分支确实被真实数据覆盖
323
+ expect(encodeCwdCount).toBeGreaterThan(0)
324
+ },
325
+ 30000,
326
+ )
327
+ })
328
+
329
+ describe('buildFamilyFromFs not-found 文案(U1 并入:列实际扫描候选根)', () => {
330
+ let tmp: string
331
+
332
+ beforeEach(async () => {
333
+ tmp = await mkdtemp(join(tmpdir(), 'roots-notfound-'))
334
+ })
335
+ afterEach(async () => {
336
+ await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
337
+ })
338
+
339
+ it('错误信息含各候选根路径、kind 标签与文件数(default/legacy),保留 findSessions 指引', async () => {
340
+ const agentDir = join(tmp, 'data', 'agent')
341
+ await writeJsonl(
342
+ join(agentDir, 'sessions', SLUG, 'known.jsonl'),
343
+ '{"type":"session","id":"known-id"}\n',
344
+ )
345
+
346
+ const msg = await buildFamilyFromFs('missing-id', agentDir).then(
347
+ () => {
348
+ throw new Error('buildFamilyFromFs should reject for unknown sessionId')
349
+ },
350
+ (e: unknown) => (e instanceof Error ? e.message : String(e)),
351
+ )
352
+ expect(msg).toContain('not found')
353
+ expect(msg).toContain(join(agentDir, 'sessions'))
354
+ expect(msg).toContain('[default]')
355
+ expect(msg).toContain('1 file(s)')
356
+ expect(msg).toContain(join(tmp, 'data', 'sessions'))
357
+ expect(msg).toContain('[legacy]')
358
+ expect(msg).toContain('not exists')
359
+ // 只列 main 候选根(collectMainSessions 实扫集合),不含 subagent 根
360
+ expect(msg).not.toContain('[subagent]')
361
+ expect(msg).toContain('findSessions')
362
+ })
363
+ })
364
+
365
+ // ============================================================
366
+ // u8 追加:resolveSessionRoots options(doctor 统一数据源,design §6.3「同一数据源
367
+ // 两处渲染,不重复实现探测」——subagent 扫描模式与进程内缓存经 options 由调用方
368
+ // 驱动,本模块不内置 TTL/mtime 失效逻辑)
369
+ // ============================================================
370
+
371
+ describe('resolveSessionRoots options(u8:subagents 模式 + 注入式缓存)', () => {
372
+ /** 最小 fixture:default 根 1 文件 + subagent 根 1 文件(legacy 不存在) */
373
+ async function buildMinimalFixture(): Promise<{ tmp: string; agentDir: string }> {
374
+ const tmp = await mkdtemp(join(tmpdir(), 'roots-opts-'))
375
+ const agentDir = join(tmp, 'agent')
376
+ await mkdir(join(agentDir, 'sessions', SLUG), { recursive: true })
377
+ await writeFile(join(agentDir, 'sessions', SLUG, 'a.jsonl'), '{"type":"session"}\n')
378
+ await mkdir(join(agentDir, 'subagents', SLUG, 'sessions'), { recursive: true })
379
+ await writeFile(join(agentDir, 'subagents', SLUG, 'sessions', 's.jsonl'), '{"type":"session"}\n')
380
+ return { tmp, agentDir }
381
+ }
382
+
383
+ it("subagents:'stat':subagent 根只 stat(无计数/耗时/files 空),main 根照常实扫", async () => {
384
+ const { tmp, agentDir } = await buildMinimalFixture()
385
+ try {
386
+ const roots = await resolveSessionRoots({ agentDir }, { subagents: 'stat' })
387
+ const sub = roots.find((r) => r.kind === 'subagent')!
388
+ expect(sub.exists).toBe(true)
389
+ expect(sub.fileCount).toBeUndefined()
390
+ expect(sub.scanMs).toBeUndefined()
391
+ expect(sub.files).toEqual([])
392
+ expect(sub.dedupedInto).toBeUndefined()
393
+ // main 根不受影响:照常实扫
394
+ const def = roots.find((r) => r.kind === 'default')!
395
+ expect(def.fileCount).toBe(1)
396
+ expect(def.files).toHaveLength(1)
397
+ } finally {
398
+ await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
399
+ }
400
+ })
401
+
402
+ it("options 缺省与 subagents:'scan':subagent 根照旧全扫(现有调用方零破坏)", async () => {
403
+ const { tmp, agentDir } = await buildMinimalFixture()
404
+ try {
405
+ for (const options of [undefined, { subagents: 'scan' as const }]) {
406
+ const roots = await resolveSessionRoots({ agentDir }, options)
407
+ const sub = roots.find((r) => r.kind === 'subagent')!
408
+ expect(sub.fileCount).toBe(1)
409
+ expect(sub.files).toHaveLength(1)
410
+ }
411
+ } finally {
412
+ await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
413
+ }
414
+ })
415
+
416
+ it('cache:未命中实扫后 set 回写;命中以缓存值构造根(cached 标记、files 空、不再回写)', async () => {
417
+ const { tmp, agentDir } = await buildMinimalFixture()
418
+ try {
419
+ const store = new Map<string, { exists: boolean; fileCount: number; scanMs: number }>()
420
+ const setSpy = vi.fn((key: string, value: { exists: boolean; fileCount: number; scanMs: number }) => {
421
+ store.set(key, value)
422
+ })
423
+ const getSpy = vi.fn(async (key: string) => store.get(key))
424
+ const cache = { get: getSpy, set: setSpy }
425
+
426
+ // 首扫:get 未命中 → 实扫 → 逐非去重根 set(default + legacy + subagent = 3)
427
+ const first = await resolveSessionRoots({ agentDir }, { cache })
428
+ const firstDef = first.find((r) => r.kind === 'default')!
429
+ expect(firstDef.cached).toBeUndefined()
430
+ expect(firstDef.fileCount).toBe(1)
431
+ expect(getSpy).toHaveBeenCalled()
432
+ expect(setSpy).toHaveBeenCalledTimes(3)
433
+
434
+ // 二扫:命中 → 缓存值即数据源,无新回写
435
+ const second = await resolveSessionRoots({ agentDir }, { cache })
436
+ const secondDef = second.find((r) => r.kind === 'default')!
437
+ expect(secondDef.cached).toBe(true)
438
+ expect(secondDef.fileCount).toBe(1)
439
+ expect(secondDef.files).toEqual([])
440
+ expect(setSpy).toHaveBeenCalledTimes(3)
441
+ } finally {
442
+ await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
443
+ }
444
+ })
445
+
446
+ it('cache 命中值优先于磁盘(roots 不旁路缓存实扫——TTL/mtime 失效语义全在缓存实现侧)', async () => {
447
+ const { tmp, agentDir } = await buildMinimalFixture()
448
+ try {
449
+ const setSpy = vi.fn()
450
+ const cache = {
451
+ get: async () => ({ exists: true, fileCount: 99, scanMs: 7 }),
452
+ set: setSpy,
453
+ }
454
+ const roots = await resolveSessionRoots({ agentDir }, { cache })
455
+ const def = roots.find((r) => r.kind === 'default')!
456
+ expect(def.fileCount).toBe(99)
457
+ expect(def.scanMs).toBe(7)
458
+ expect(def.cached).toBe(true)
459
+ expect(def.files).toEqual([])
460
+ expect(setSpy).not.toHaveBeenCalled() // 命中不回写
461
+ } finally {
462
+ await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
463
+ }
464
+ })
465
+
466
+ it("stat × cache 组合:'stat' 的 subagent 根不产缓存条目(缓存只服务扫盘贵的根)", async () => {
467
+ const { tmp, agentDir } = await buildMinimalFixture()
468
+ try {
469
+ const setKeys: string[] = []
470
+ await resolveSessionRoots({ agentDir }, {
471
+ subagents: 'stat',
472
+ cache: {
473
+ get: async () => undefined,
474
+ set: (key) => {
475
+ setKeys.push(key)
476
+ },
477
+ },
478
+ })
479
+ expect(setKeys).not.toContain(join(agentDir, 'subagents'))
480
+ expect(setKeys).toContain(join(agentDir, 'sessions'))
481
+ } finally {
482
+ await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
483
+ }
484
+ })
132
485
  })
@@ -72,7 +72,7 @@ describe('createSessionCommand - getArgumentCompletions', () => {
72
72
  await mkdir(cwdSessionDir, { recursive: true })
73
73
  })
74
74
  afterEach(async () => {
75
- await rm(agentDir, { recursive: true, force: true })
75
+ await rm(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
76
76
  })
77
77
 
78
78
  it('空参数 → recent 列表(value=完整 uuid 剥 #,label 含预览)', async () => {
@@ -129,7 +129,7 @@ describe('createSessionCommand - handler select 流程', () => {
129
129
  await mkdir(cwdSessionDir, { recursive: true })
130
130
  })
131
131
  afterEach(async () => {
132
- await rm(agentDir, { recursive: true, force: true })
132
+ await rm(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
133
133
  })
134
134
 
135
135
  it('选中 label → setEditorText(#完整 uuid + 尾随空格)(value 语义保持完整 uuid)', async () => {
@@ -10,6 +10,7 @@ import {
10
10
  extractSessionIdFromFilename,
11
11
  type RecordManifest,
12
12
  } from '../discovery/subagents.js'
13
+ import { REAL_DATA_TIMEOUT_MS } from './real-data.js'
13
14
 
14
15
  // ---- fixture 常量(uuid 特征,满足 extractSessionIdFromFilename + 互不为子串)----
15
16
  const ROOT = '0aaaaaaa-bbbb-7ccc-dddd-000000000001'
@@ -196,7 +197,7 @@ describe('buildFamilyFromFs - fixture', () => {
196
197
  dir = await makeAgentDir()
197
198
  })
198
199
  afterEach(async () => {
199
- await rm(dir, { recursive: true, force: true })
200
+ await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
200
201
  })
201
202
 
202
203
  it('基础 family:root + fork + 隔代 subagent,SubagentRef.sessionId 是真实 id(非 sa-xxx)', async () => {
@@ -471,7 +472,7 @@ describe.skipIf(!HAS_REAL_ROOT)('buildFamilyFromFs - 真实数据 ~/.pi/agent',
471
472
  )
472
473
  expect(sub).toBeDefined()
473
474
  expect(sub!.sessionId.startsWith('sa-')).toBe(false)
474
- }, 30000)
475
+ }, REAL_DATA_TIMEOUT_MS)
475
476
  })
476
477
 
477
478
  describe.skipIf(!HAS_REAL_WF)('buildFamilyFromFs - 真实 workflow 数据', () => {
@@ -500,7 +501,7 @@ describe.skipIf(!HAS_REAL_WF)('buildFamilyFromFs - 真实 workflow 数据', () =
500
501
  // stateFile 是 wf-state 文件绝对路径
501
502
  expect(rich!.stateFile.endsWith('.jsonl')).toBe(true)
502
503
  expect(rich!.runId.startsWith('wf-')).toBe(true)
503
- }, 30000)
504
+ }, REAL_DATA_TIMEOUT_MS)
504
505
  })
505
506
 
506
507
  // ============================================================
@@ -514,7 +515,7 @@ describe('listRecordManifests 导出(w2 TC1)', () => {
514
515
  dir = await makeAgentDir()
515
516
  })
516
517
  afterEach(async () => {
517
- await rm(dir, { recursive: true, force: true })
518
+ await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
518
519
  })
519
520
 
520
521
  it('导出生效:import + 调用返回 RecordManifest[],字段完整', async () => {
@@ -545,7 +546,7 @@ describe('U4 buildFamilyFromFs 富化(manifest 主 / P-fallback)', () => {
545
546
  dir = await makeAgentDir()
546
547
  })
547
548
  afterEach(async () => {
548
- await rm(dir, { recursive: true, force: true })
549
+ await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
549
550
  })
550
551
 
551
552
  it('TC-u4-manifest-enrich: alive + manifest 全字段 → SubagentRef 富字段全透传', async () => {
@@ -735,5 +736,5 @@ describe.skipIf(!HAS_REAL_ROOT || !HAS_REAL_SUBAGENTS_DIR)('U4 真实数据守
735
736
 
736
737
  // 所有 subagent 的 sessionFile 非空(manifest.sessionFile 或 alive meta.path)
737
738
  expect(family.subagents.every((s) => typeof s.sessionFile === 'string' && s.sessionFile.length > 0)).toBe(true)
738
- }, 30000)
739
+ }, REAL_DATA_TIMEOUT_MS)
739
740
  })