@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.
- package/package.json +3 -2
- package/src/__tests__/cross-package-subagent-core.test.ts +321 -0
- package/src/__tests__/env.test.ts +258 -0
- package/src/__tests__/execution-tree.test.ts +9 -8
- package/src/__tests__/find.test.ts +214 -9
- package/src/__tests__/hash-provider.test.ts +2 -2
- package/src/__tests__/index.test.ts +157 -5
- package/src/__tests__/real-data.ts +10 -0
- package/src/__tests__/result.test.ts +341 -0
- package/src/__tests__/roots.test.ts +362 -9
- package/src/__tests__/session-command.test.ts +2 -2
- package/src/__tests__/subagents.test.ts +7 -6
- package/src/__tests__/tool-handler.test.ts +1216 -7
- package/src/discovery/env.ts +238 -0
- package/src/discovery/find.ts +363 -56
- package/src/discovery/roots.ts +236 -8
- package/src/discovery/subagents.ts +31 -11
- package/src/doctor.ts +262 -0
- package/src/extract.ts +422 -0
- package/src/handler-utils.ts +63 -0
- package/src/index.ts +63 -9
- package/src/no-match.ts +160 -0
- package/src/result-action.ts +244 -0
- package/src/search-across.ts +539 -0
- package/src/tool-handler.ts +554 -723
- package/vitest.config.ts +15 -0
|
@@ -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 {
|
|
6
|
-
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
739
|
+
}, REAL_DATA_TIMEOUT_MS)
|
|
739
740
|
})
|