@zhushanwen/pi-session-reader 0.5.1 → 0.6.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.
@@ -2,11 +2,8 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
2
  import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
- import {
6
- handleSessionRead,
7
- extractFinalAssistantText,
8
- type SessionReadParams,
9
- } from '../tool-handler.js'
5
+ import { handleSessionRead, type SessionReadParams } from '../tool-handler.js'
6
+ import { extractFinalAssistantText } from '../result-action.js'
10
7
 
11
8
  /**
12
9
  * U6 result action 测试(design subagent-sync-collect §3.1.3 + impl-plan U6)。
@@ -152,7 +149,7 @@ describe('result action(U6)', () => {
152
149
  assistantEntry(`${realId}-a2`, [{ type: 'text', text: 'second part' }]),
153
150
  ],
154
151
  })
155
- const r = await handleSessionRead({ action: 'result', session: 'sa-one' }, dir)
152
+ const r = await handleSessionRead({ action: 'result', session: 'sa-one' }, { agentDir: dir })
156
153
  // 与 record.result 同源:全部 assistant 正文非空过滤 '\n\n' join,无任何包装
157
154
  expect(r.content[0]?.text).toBe('FINAL RESULT\n\nsecond part')
158
155
  const d = r.details as {
@@ -184,7 +181,7 @@ describe('result action(U6)', () => {
184
181
  }
185
182
  const r = await handleSessionRead(
186
183
  { action: 'result', session: 'sa-b1,sa-b2,sa-b3' },
187
- dir,
184
+ { agentDir: dir },
188
185
  )
189
186
  const text = r.content[0]?.text ?? ''
190
187
  expect(text).toContain('[1/3] sa-b1')
@@ -207,10 +204,10 @@ describe('result action(U6)', () => {
207
204
  it('3. 批量 >10:11 id 拒绝,报错含上限 10 与分批指引(解析先于 fs,无需 fixture)', async () => {
208
205
  const list = Array.from({ length: 11 }, (_, i) => `sa-x${i}`).join(',')
209
206
  await expect(
210
- handleSessionRead({ action: 'result', session: list }, dir),
207
+ handleSessionRead({ action: 'result', session: list }, { agentDir: dir }),
211
208
  ).rejects.toThrow(/最多 10 个/)
212
209
  try {
213
- await handleSessionRead({ action: 'result', session: list }, dir)
210
+ await handleSessionRead({ action: 'result', session: list }, { agentDir: dir })
214
211
  } catch (e) {
215
212
  const msg = (e as Error).message
216
213
  expect(msg).toContain('11')
@@ -226,7 +223,7 @@ describe('result action(U6)', () => {
226
223
  realId,
227
224
  entries: [assistantEntry(`${realId}-a1`, [{ type: 'text', text: longText }])],
228
225
  })
229
- const r = await handleSessionRead({ action: 'result', session: 'sa-long' }, dir)
226
+ const r = await handleSessionRead({ action: 'result', session: 'sa-long' }, { agentDir: dir })
230
227
  const text = r.content[0]?.text ?? ''
231
228
  // 截断到 8000:第 8001 个 x 不在正文,但提示行给出计数
232
229
  expect(text.startsWith('x'.repeat(8000))).toBe(true)
@@ -249,7 +246,7 @@ describe('result action(U6)', () => {
249
246
  })
250
247
  const cut = await handleSessionRead(
251
248
  { action: 'result', session: 'sa-lim', limit: 5 },
252
- dir,
249
+ { agentDir: dir },
253
250
  )
254
251
  expect(cut.content[0]?.text).toBe(
255
252
  'abcde\n\n[truncated 5 of 10 chars — full text: read ' +
@@ -258,31 +255,31 @@ describe('result action(U6)', () => {
258
255
  )
259
256
  const exact = await handleSessionRead(
260
257
  { action: 'result', session: 'sa-lim', limit: 10 },
261
- dir,
258
+ { agentDir: dir },
262
259
  )
263
260
  expect(exact.content[0]?.text).toBe('abcdefghij')
264
261
  const d = exact.details as { truncated: boolean }
265
262
  expect(d.truncated).toBe(false)
266
263
  for (const bad of [0, -1, Number.NaN]) {
267
264
  await expect(
268
- handleSessionRead({ action: 'result', session: 'sa-lim', limit: bad }, dir),
265
+ handleSessionRead({ action: 'result', session: 'sa-lim', limit: bad }, { agentDir: dir }),
269
266
  ).rejects.toThrow(/limit.*无效/)
270
267
  }
271
268
  })
272
269
 
273
270
  it('6. 不存在 id:sa-id 无 manifest → 无匹配 record;uuid 片段零匹配 → 无匹配 session', async () => {
274
271
  await expect(
275
- handleSessionRead({ action: 'result', session: 'sa-nonexist-9999' }, dir),
272
+ handleSessionRead({ action: 'result', session: 'sa-nonexist-9999' }, { agentDir: dir }),
276
273
  ).rejects.toThrow(/无匹配 record/)
277
274
  try {
278
- await handleSessionRead({ action: 'result', session: 'sa-nonexist-9999' }, dir)
275
+ await handleSessionRead({ action: 'result', session: 'sa-nonexist-9999' }, { agentDir: dir })
279
276
  } catch (e) {
280
277
  const msg = (e as Error).message
281
278
  expect(msg).toContain('👉')
282
279
  expect(msg).toContain('family')
283
280
  }
284
281
  await expect(
285
- handleSessionRead({ action: 'result', session: 'zzz-nonexistent-9q8x2' }, dir),
282
+ handleSessionRead({ action: 'result', session: 'zzz-nonexistent-9q8x2' }, { agentDir: dir }),
286
283
  ).rejects.toThrow(/无匹配 session/)
287
284
  })
288
285
 
@@ -293,10 +290,10 @@ describe('result action(U6)', () => {
293
290
  sessionFileExists: false,
294
291
  })
295
292
  await expect(
296
- handleSessionRead({ action: 'result', session: 'sa-flush' }, dir),
293
+ handleSessionRead({ action: 'result', session: 'sa-flush' }, { agentDir: dir }),
297
294
  ).rejects.toThrow(/session 文件不存在/)
298
295
  try {
299
- await handleSessionRead({ action: 'result', session: 'sa-flush' }, dir)
296
+ await handleSessionRead({ action: 'result', session: 'sa-flush' }, { agentDir: dir })
300
297
  } catch (e) {
301
298
  const msg = (e as Error).message
302
299
  expect(msg).toContain('未写入')
@@ -310,10 +307,10 @@ describe('result action(U6)', () => {
310
307
  realId: '019e6c96-0000-0000-0000-000000000a01',
311
308
  })
312
309
  await expect(
313
- handleSessionRead({ action: 'result', session: 'sa-empty' }, dir),
310
+ handleSessionRead({ action: 'result', session: 'sa-empty' }, { agentDir: dir }),
314
311
  ).rejects.toThrow(/尚无 assistant 输出/)
315
312
  try {
316
- await handleSessionRead({ action: 'result', session: 'sa-empty' }, dir)
313
+ await handleSessionRead({ action: 'result', session: 'sa-empty' }, { agentDir: dir })
317
314
  } catch (e) {
318
315
  const msg = (e as Error).message
319
316
  expect(msg).toContain('sa-empty')
@@ -323,10 +320,10 @@ describe('result action(U6)', () => {
323
320
 
324
321
  it('9. 参数防御:缺 session / 空条目(尾逗号)均报错', async () => {
325
322
  await expect(
326
- handleSessionRead({ action: 'result' } as SessionReadParams, dir),
323
+ handleSessionRead({ action: 'result' } as SessionReadParams, { agentDir: dir }),
327
324
  ).rejects.toThrow(/需要参数 "session"/)
328
325
  await expect(
329
- handleSessionRead({ action: 'result', session: 'sa-a,' }, dir),
326
+ handleSessionRead({ action: 'result', session: 'sa-a,' }, { agentDir: dir }),
330
327
  ).rejects.toThrow(/空条目/)
331
328
  })
332
329
 
@@ -334,7 +331,7 @@ describe('result action(U6)', () => {
334
331
  await expect(
335
332
  handleSessionRead(
336
333
  { action: 'bogus' } as unknown as SessionReadParams,
337
- dir,
334
+ { agentDir: dir },
338
335
  ),
339
336
  ).rejects.toThrow(/find\/family\/outline\/expand\/detail\/search\/export\/extract\/workflow\/result\/doctor/)
340
337
  })
@@ -1,10 +1,8 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { mkdtemp, mkdir, readdir, writeFile, rm } from 'node:fs/promises'
4
4
  import { join, dirname } from 'node:path'
5
5
  import {
6
- listMainSessions,
7
- listSubagentSessions,
8
6
  resolveSessionRoots,
9
7
  normalizeLiveSessionDir,
10
8
  type SessionRoot,
@@ -13,6 +11,24 @@ import {
13
11
  import { buildFamilyFromFs } from '../discovery/subagents.js'
14
12
  import { REAL_AGENT_DIR, HAS_E6, HAS_REAL_AGENT_DIR, HAS_REAL_SUBAGENTS_DIR, REAL_DATA_TIMEOUT_MS } from './real-data.js'
15
13
 
14
+ // ============================================================
15
+ // A3(ext-simplify-04 U4):listMainSessions/listSubagentSessions 薄包装已删除,
16
+ // subagents.ts 直调 resolveSessionRoots+filter。以下 describe 用同一等价式作入口,
17
+ // 保过滤语义覆盖(.finalized 排除 / workflow-state 跳过 / 递归 / 根选择 / 空目录容错)。
18
+ // ============================================================
19
+
20
+ /** 原 listMainSessions 等价式:agentDir 信号包下未去重 main 根的文件并集 */
21
+ async function mainSessionFiles(agentDir: string) {
22
+ return (await resolveSessionRoots({ agentDir }))
23
+ .filter((r) => r.source === 'main' && r.dedupedInto === undefined)
24
+ .flatMap((r) => r.files)
25
+ }
26
+
27
+ /** 原 listSubagentSessions 等价式:agentDir 信号包下 [subagent] 根文件集 */
28
+ async function subagentSessionFiles(agentDir: string) {
29
+ return (await resolveSessionRoots({ agentDir })).find((r) => r.kind === 'subagent')?.files ?? []
30
+ }
31
+
16
32
  /** 写 .jsonl fixture(父目录不存在则自建),内容默认合法 session header */
17
33
  async function writeJsonl(path: string, content = '{"type":"session","id":"x"}\n') {
18
34
  await mkdir(dirname(path), { recursive: true })
@@ -22,7 +38,7 @@ async function writeJsonl(path: string, content = '{"type":"session","id":"x"}\n
22
38
  /** 真实形态的 cwd 编码目录名(encodeCwd:`--` 开头 `--` 结尾,实证见 real-data.ts) */
23
39
  const SLUG = '--Users-foo--'
24
40
 
25
- describe('listMainSessions', () => {
41
+ describe('main 根过滤(原 listMainSessions 等价式,A3 退役后保覆盖)', () => {
26
42
  let dir: string
27
43
 
28
44
  beforeEach(async () => {
@@ -38,7 +54,7 @@ describe('listMainSessions', () => {
38
54
  await writeFile(join(dir, 'sessions', slug, 'a.jsonl'), '{"type":"session","id":"a"}\n')
39
55
  await writeFile(join(dir, 'sessions', slug, 'b.jsonl.finalized'), '{"type":"session","id":"b"}\n')
40
56
 
41
- const result = await listMainSessions(dir)
57
+ const result = await mainSessionFiles(dir)
42
58
  expect(result).toHaveLength(1)
43
59
  expect(result[0].path.endsWith('a.jsonl')).toBe(true)
44
60
  expect(result[0].path.endsWith('.finalized')).toBe(false)
@@ -54,7 +70,7 @@ describe('listMainSessions', () => {
54
70
  // 同 slug 直接层也放一个,验证同一 slug 下平铺与嵌套并存
55
71
  await writeFile(join(dir, 'sessions', 'slug', 'top.jsonl'), '{}\n')
56
72
 
57
- const result = await listMainSessions(dir)
73
+ const result = await mainSessionFiles(dir)
58
74
  const names = result.map((m) => m.path.split('/').pop()!)
59
75
  expect(names).toContain('d.jsonl')
60
76
  expect(names).toContain('top.jsonl')
@@ -70,22 +86,22 @@ describe('listMainSessions', () => {
70
86
  '{"v":"wf-run-v1"}\n',
71
87
  )
72
88
 
73
- const result = await listMainSessions(dir)
89
+ const result = await mainSessionFiles(dir)
74
90
  expect(result).toHaveLength(1)
75
91
  expect(result[0].path.endsWith('real.jsonl')).toBe(true)
76
92
  expect(result.every((m) => !m.path.includes('workflow-state'))).toBe(true)
77
93
  })
78
94
 
79
95
  it('空 agentDir(无 sessions 目录)返回 [],不抛错', async () => {
80
- await expect(listMainSessions(dir)).resolves.toEqual([])
96
+ await expect(mainSessionFiles(dir)).resolves.toEqual([])
81
97
  })
82
98
 
83
99
  it('不存在的 agentDir 返回 [],不抛错', async () => {
84
- await expect(listMainSessions(join(dir, 'no-such-dir'))).resolves.toEqual([])
100
+ await expect(mainSessionFiles(join(dir, 'no-such-dir'))).resolves.toEqual([])
85
101
  })
86
102
 
87
103
  it.skipIf(!HAS_E6)('真实数据:扫描 ~/.pi/agent,含 019e6c96,不含 .finalized 与 wf-', async () => {
88
- const result = await listMainSessions(REAL_AGENT_DIR)
104
+ const result = await mainSessionFiles(REAL_AGENT_DIR)
89
105
  expect(result.length).toBeGreaterThan(0)
90
106
  // 含目标 session
91
107
  expect(result.some((m) => m.path.includes('019e6c96'))).toBe(true)
@@ -100,7 +116,7 @@ describe('listMainSessions', () => {
100
116
  }, REAL_DATA_TIMEOUT_MS)
101
117
  })
102
118
 
103
- describe('listSubagentSessions', () => {
119
+ describe('subagent 根选择(原 listSubagentSessions 等价式,A3 退役后保覆盖)', () => {
104
120
  let dir: string
105
121
 
106
122
  beforeEach(async () => {
@@ -119,7 +135,7 @@ describe('listSubagentSessions', () => {
119
135
  '{"type":"session"}\n',
120
136
  )
121
137
 
122
- const result = await listSubagentSessions(dir)
138
+ const result = await subagentSessionFiles(dir)
123
139
  expect(result).toHaveLength(1)
124
140
  expect(result[0].path.endsWith('c.jsonl')).toBe(true)
125
141
  expect(result[0].path.endsWith('.finalized')).toBe(false)
@@ -132,17 +148,17 @@ describe('listSubagentSessions', () => {
132
148
  await writeFile(join(dir, 'subagents', slug, 'records', 'manifest.json'), '{}\n')
133
149
  await writeFile(join(dir, 'subagents', slug, 'sessions', 'sub.jsonl'), '{"type":"session"}\n')
134
150
 
135
- const result = await listSubagentSessions(dir)
151
+ const result = await subagentSessionFiles(dir)
136
152
  expect(result).toHaveLength(1)
137
153
  expect(result[0].path.endsWith('sub.jsonl')).toBe(true)
138
154
  })
139
155
 
140
156
  it('无 subagents 目录返回 [],不抛错', async () => {
141
- await expect(listSubagentSessions(dir)).resolves.toEqual([])
157
+ await expect(subagentSessionFiles(dir)).resolves.toEqual([])
142
158
  })
143
159
 
144
160
  it.skipIf(!HAS_REAL_SUBAGENTS_DIR)('真实数据:扫描 ~/.pi/agent/subagents 返回非空', async () => {
145
- const result = await listSubagentSessions(REAL_AGENT_DIR)
161
+ const result = await subagentSessionFiles(REAL_AGENT_DIR)
146
162
  expect(result.length).toBeGreaterThan(0)
147
163
  expect(result.every((m) => !m.path.endsWith('.finalized'))).toBe(true)
148
164
  }, REAL_DATA_TIMEOUT_MS)
@@ -198,7 +214,7 @@ const HOST_CASES: HostCase[] = [
198
214
  },
199
215
  },
200
216
  {
201
- name: 'xyz-agent:live=根本身(RPC 覆盖态),default 去重,legacy 残留与 subagent 独立列出',
217
+ name: 'taiji:live=根本身(RPC 覆盖态),default 去重,legacy 残留与 subagent 独立列出',
202
218
  async build(tmp) {
203
219
  const dataDir = join(tmp, 'data')
204
220
  const agentDir = join(dataDir, 'agent')
@@ -364,11 +380,11 @@ describe('buildFamilyFromFs not-found 文案(U1 并入:列实际扫描候选
364
380
 
365
381
  // ============================================================
366
382
  // u8 追加:resolveSessionRoots options(doctor 统一数据源,design §6.3「同一数据源
367
- // 两处渲染,不重复实现探测」——subagent 扫描模式与进程内缓存经 options 由调用方
368
- // 驱动,本模块不内置 TTL/mtime 失效逻辑)
383
+ // 两处渲染,不重复实现探测」——subagent 扫描模式经 options 由调用方驱动;原 u8 的
384
+ // 注入式缓存面已删除,ext-simplify-04 U3,每次调用实扫)
369
385
  // ============================================================
370
386
 
371
- describe('resolveSessionRoots options(u8:subagents 模式 + 注入式缓存)', () => {
387
+ describe('resolveSessionRoots options(u8:subagents 模式)', () => {
372
388
  /** 最小 fixture:default 根 1 文件 + subagent 根 1 文件(legacy 不存在) */
373
389
  async function buildMinimalFixture(): Promise<{ tmp: string; agentDir: string }> {
374
390
  const tmp = await mkdtemp(join(tmpdir(), 'roots-opts-'))
@@ -413,71 +429,21 @@ describe('resolveSessionRoots options(u8:subagents 模式 + 注入式缓存
413
429
  }
414
430
  })
415
431
 
416
- it('cache:未命中实扫后 set 回写;命中以缓存值构造根(cached 标记、files 空、不再回写)', async () => {
432
+ it('重复调用每次实扫(无缓存):两次结果独立完整,files 恒非空实扫值', async () => {
433
+ // 原三个 cache 用例(set 回写 / 命中优先 / stat × cache)随缓存注入面删除
434
+ // (ext-simplify-04 U3);本用例钉死删除后契约——不存在任何缓存层
417
435
  const { tmp, agentDir } = await buildMinimalFixture()
418
436
  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,
437
+ const first = await resolveSessionRoots({ agentDir })
438
+ const second = await resolveSessionRoots({ agentDir })
439
+ for (const roots of [first, second]) {
440
+ const def = roots.find((r) => r.kind === 'default')!
441
+ expect(def.fileCount).toBe(1)
442
+ expect(def.files).toHaveLength(1)
443
+ const sub = roots.find((r) => r.kind === 'subagent')!
444
+ expect(sub.fileCount).toBe(1)
445
+ expect(sub.files).toHaveLength(1)
453
446
  }
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
447
  } finally {
482
448
  await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
483
449
  }
@@ -3,6 +3,7 @@ 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 { createSessionCommand } from '../tui/session-command.js'
6
+ import { SessionManager } from '@earendil-works/pi-coding-agent'
6
7
  import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent'
7
8
 
8
9
  /**
@@ -117,6 +118,16 @@ describe('createSessionCommand - getArgumentCompletions', () => {
117
118
  const cmd = createSessionCommand(() => cwdSessionDir)
118
119
  expect(await cmd.getArgumentCompletions('deadbeef')).toBeNull()
119
120
  })
121
+
122
+ it('空 getCwdSessionDir → 秒回 null,不调 listAll(A1:无 live session 上下文不触发全盘分支)', async () => {
123
+ // mock 拦断言用:guard 失效误调 listAll('') 时 pi 会走真实全盘扫描(实测 3488 项 / ~8s),
124
+ // mock 掉保证失败态也是秒级红灯
125
+ const listAllSpy = vi.spyOn(SessionManager, 'listAll').mockResolvedValue([])
126
+ const cmd = createSessionCommand(() => '')
127
+ expect(await cmd.getArgumentCompletions('')).toBeNull()
128
+ expect(listAllSpy).not.toHaveBeenCalled()
129
+ listAllSpy.mockRestore()
130
+ })
120
131
  })
121
132
 
122
133
  describe('createSessionCommand - handler select 流程', () => {
@@ -175,6 +186,18 @@ describe('createSessionCommand - handler select 流程', () => {
175
186
  expect(setEditorText).not.toHaveBeenCalled()
176
187
  })
177
188
 
189
+ it('空 getCwdSessionDir → 按零匹配 notify,不调 listAll(A1:handler 侧同款空串 guard)', async () => {
190
+ const listAllSpy = vi.spyOn(SessionManager, 'listAll').mockResolvedValue([])
191
+ const cmd = createSessionCommand(() => '')
192
+ const { ctx, select, notify, setEditorText } = makeFakeCtx()
193
+ await cmd.handler('', ctx)
194
+ expect(notify).toHaveBeenCalledWith('未找到匹配的 session。', 'warning')
195
+ expect(select).not.toHaveBeenCalled()
196
+ expect(setEditorText).not.toHaveBeenCalled()
197
+ expect(listAllSpy).not.toHaveBeenCalled()
198
+ listAllSpy.mockRestore()
199
+ })
200
+
178
201
  it('MF-2 回归:同 cwd 同预览同 age 桶 → label 带短 uuid 后缀消歧,选第 2 条插第 2 条 uuid', async () => {
179
202
  // 两 session:相同首消息 + 相同 timestamp(同 age 桶)→ toCandidate label 完全相同
180
203
  const ID1 = '019e6c96-0a0c-74b8-a73f-d1854d88e2a7'
@@ -10,16 +10,13 @@ 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
+ import { REAL_AGENT_DIR, REAL_DATA_TIMEOUT_MS } from './real-data.js'
14
14
 
15
15
  // ---- fixture 常量(uuid 特征,满足 extractSessionIdFromFilename + 互不为子串)----
16
16
  const ROOT = '0aaaaaaa-bbbb-7ccc-dddd-000000000001'
17
17
  const FORK = '0aaaaaaa-bbbb-7ccc-dddd-000000000002'
18
18
  const SUB_REAL = '0aaaaaaa-bbbb-7ccc-dddd-000000000003'
19
19
 
20
- /** 真实 pi agent 目录(本机),用于集成测试。 */
21
- const REAL_AGENT_DIR = '/Users/zhushanwen/.pi/agent'
22
-
23
20
  // 同步探测真实 session 是否存在(不存在则 skip,避免在无该数据的机器上硬失败)
24
21
  function hasRealSession(sid: string): boolean {
25
22
  try {
@@ -228,9 +225,10 @@ describe('buildFamilyFromFs - fixture', () => {
228
225
  expect(sub!.rootSessionId).toBe(FORK)
229
226
  expect(sub!.slug).toBe('test-sub')
230
227
  expect(sub!.cleanedUp).toBe(false)
231
- // enrich:fileName/cwd 已补真实值(非 M1 占位空串)
232
- expect(sub!.fileName.length).toBeGreaterThan(0)
233
- expect(sub!.cwd).toBe('/proj/--fork-cwd--')
228
+ // enrichRefs 已删除(ext-simplify-04 U4/E2):family 路径 fileName/cwd 维持 core 占位空串
229
+ //(SessionRef 与 find 路径共享,find 侧有真实消费者;workflow calls 反查走 pathToRef 不受影响)
230
+ expect(sub!.fileName).toBe('')
231
+ expect(sub!.cwd).toBe('')
234
232
  })
235
233
 
236
234
  it('从 fork 子代 resolve 也能关联到挂在其下的 subagent', async () => {