@zhushanwen/pi-session-reader 0.5.0 → 0.6.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 -2
- package/src/__tests__/cross-package-subagent-core.test.ts +13 -10
- package/src/__tests__/execution-tree.test.ts +139 -31
- package/src/__tests__/family.test.ts +0 -7
- package/src/__tests__/find.test.ts +5 -5
- package/src/__tests__/real-data.ts +1 -1
- package/src/__tests__/rebuild-manifest-dual-write.test.ts +177 -0
- package/src/__tests__/render.test.ts +36 -1
- package/src/__tests__/result.test.ts +20 -23
- package/src/__tests__/roots.test.ts +46 -80
- package/src/__tests__/session-command.test.ts +23 -0
- package/src/__tests__/subagents.test.ts +4 -3
- package/src/__tests__/tool-handler.test.ts +419 -141
- package/src/__tests__/workflow.test.ts +1 -7
- package/src/core/execution-tree.ts +1 -0
- package/src/core/family.ts +12 -9
- package/src/core/render.ts +40 -12
- package/src/core/workflow.ts +7 -20
- package/src/discovery/find.ts +42 -134
- package/src/discovery/roots.ts +16 -87
- package/src/discovery/session-header.ts +103 -0
- package/src/discovery/subagents.ts +23 -94
- package/src/doctor.ts +11 -77
- package/src/extract.ts +6 -19
- package/src/handler-utils.ts +28 -5
- package/src/no-match.ts +3 -2
- package/src/result-action.ts +30 -35
- package/src/search-across.ts +5 -4
- package/src/tool-handler.ts +151 -176
- package/src/tui/hash-provider.ts +7 -3
- package/src/tui/session-command.ts +22 -10
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
96
|
+
await expect(mainSessionFiles(dir)).resolves.toEqual([])
|
|
81
97
|
})
|
|
82
98
|
|
|
83
99
|
it('不存在的 agentDir 返回 [],不抛错', async () => {
|
|
84
|
-
await expect(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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)
|
|
@@ -364,11 +380,11 @@ describe('buildFamilyFromFs not-found 文案(U1 并入:列实际扫描候选
|
|
|
364
380
|
|
|
365
381
|
// ============================================================
|
|
366
382
|
// u8 追加:resolveSessionRoots options(doctor 统一数据源,design §6.3「同一数据源
|
|
367
|
-
// 两处渲染,不重复实现探测」——subagent
|
|
368
|
-
//
|
|
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('
|
|
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
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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'
|
|
@@ -228,9 +228,10 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
228
228
|
expect(sub!.rootSessionId).toBe(FORK)
|
|
229
229
|
expect(sub!.slug).toBe('test-sub')
|
|
230
230
|
expect(sub!.cleanedUp).toBe(false)
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
expect(sub!.
|
|
231
|
+
// enrichRefs 已删除(ext-simplify-04 U4/E2):family 路径 fileName/cwd 维持 core 占位空串
|
|
232
|
+
//(SessionRef 与 find 路径共享,find 侧有真实消费者;workflow calls 反查走 pathToRef 不受影响)
|
|
233
|
+
expect(sub!.fileName).toBe('')
|
|
234
|
+
expect(sub!.cwd).toBe('')
|
|
234
235
|
})
|
|
235
236
|
|
|
236
237
|
it('从 fork 子代 resolve 也能关联到挂在其下的 subagent', async () => {
|