@zhushanwen/pi-session-reader 0.13.1 → 0.13.3
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 -5
- package/src/__tests__/entry-anchor.test.ts +704 -324
- package/src/__tests__/family.test.ts +588 -312
- package/src/__tests__/{cross-package-subagent-core.test.ts → path-encoding.test.ts} +194 -30
- package/src/__tests__/roots-merged.test.ts +4919 -0
- package/src/__tests__/roots.test.ts +1986 -432
- package/src/__tests__/tool-handler.test.ts +872 -3216
- package/src/__tests__/tree.test.ts +738 -101
- package/src/__tests__/archive-ledger.test.ts +0 -447
- package/src/__tests__/execution-tree.test.ts +0 -898
- package/src/__tests__/family-zcode.test.ts +0 -265
- package/src/__tests__/find.test.ts +0 -802
- package/src/__tests__/rebuild-manifest-dual-write.test.ts +0 -188
- package/src/__tests__/render.test.ts +0 -481
- package/src/__tests__/result.test.ts +0 -347
- package/src/__tests__/subagents.test.ts +0 -668
- package/src/__tests__/summary.test.ts +0 -223
- package/src/__tests__/turns.test.ts +0 -144
- package/src/__tests__/zcode-manifest.test.ts +0 -372
- package/src/__tests__/zcode-routing.test.ts +0 -772
|
@@ -1,802 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, rm, utimes } from 'node:fs/promises'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { findSessions } from '../discovery/find.js'
|
|
6
|
-
import {
|
|
7
|
-
resolveStoreSubagentSessionsDir,
|
|
8
|
-
resolveStoreSubagentRecordsDir,
|
|
9
|
-
} from '../discovery/roots.js'
|
|
10
|
-
import { FIX_E6 } from './fixtures.js'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 建一个假 session 文件:首行 header(type=session,含 id/cwd/parentSession),
|
|
14
|
-
* 可选第二条 user message(用于名称关键词匹配 + firstMessagePreview)。返回绝对路径。
|
|
15
|
-
*/
|
|
16
|
-
async function makeSession(
|
|
17
|
-
dir: string,
|
|
18
|
-
opts: {
|
|
19
|
-
name: string
|
|
20
|
-
id: string
|
|
21
|
-
cwd?: string
|
|
22
|
-
parentSession?: string
|
|
23
|
-
firstUserText?: string
|
|
24
|
-
},
|
|
25
|
-
): Promise<string> {
|
|
26
|
-
const header: Record<string, unknown> = {
|
|
27
|
-
type: 'session',
|
|
28
|
-
id: opts.id,
|
|
29
|
-
timestamp: '2026-01-01T00:00:00.000Z',
|
|
30
|
-
}
|
|
31
|
-
if (opts.cwd) header.cwd = opts.cwd
|
|
32
|
-
if (opts.parentSession) header.parentSession = opts.parentSession
|
|
33
|
-
const lines = [JSON.stringify(header)]
|
|
34
|
-
if (opts.firstUserText) {
|
|
35
|
-
lines.push(
|
|
36
|
-
JSON.stringify({
|
|
37
|
-
type: 'message',
|
|
38
|
-
id: opts.id + '-m1',
|
|
39
|
-
message: { role: 'user', content: [{ type: 'text', text: opts.firstUserText }] },
|
|
40
|
-
}),
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
await mkdir(dir, { recursive: true })
|
|
44
|
-
const path = join(dir, opts.name)
|
|
45
|
-
await writeFile(path, lines.join('\n') + '\n')
|
|
46
|
-
return path
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 建 records manifest(U5 fixture)。manifest 由终态 finalize 或 sync 批落标出口写入
|
|
51
|
-
* records/<sa-id>.json(创建时不写),sessionFile 指向 alive subagent session 的绝对路径
|
|
52
|
-
* (find 用 extractSessionIdFromFilename 从该路径提取 sessionId 建索引)。
|
|
53
|
-
*/
|
|
54
|
-
async function makeRecordManifest(
|
|
55
|
-
recordsDir: string,
|
|
56
|
-
opts: {
|
|
57
|
-
id: string
|
|
58
|
-
rootSessionId: string
|
|
59
|
-
sessionFile: string
|
|
60
|
-
agentName?: string
|
|
61
|
-
task?: string
|
|
62
|
-
slug?: string
|
|
63
|
-
model?: string
|
|
64
|
-
status?: string
|
|
65
|
-
},
|
|
66
|
-
): Promise<void> {
|
|
67
|
-
const m: Record<string, unknown> = {
|
|
68
|
-
id: opts.id,
|
|
69
|
-
rootSessionId: opts.rootSessionId,
|
|
70
|
-
sessionFile: opts.sessionFile,
|
|
71
|
-
}
|
|
72
|
-
if (opts.agentName !== undefined) m.agentName = opts.agentName
|
|
73
|
-
if (opts.task !== undefined) m.task = opts.task
|
|
74
|
-
if (opts.slug !== undefined) m.slug = opts.slug
|
|
75
|
-
if (opts.model !== undefined) m.model = opts.model
|
|
76
|
-
if (opts.status !== undefined) m.status = opts.status
|
|
77
|
-
await mkdir(recordsDir, { recursive: true })
|
|
78
|
-
await writeFile(join(recordsDir, `${opts.id}.json`), JSON.stringify(m))
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 建 subagent session 文件(header + 可选首消息 + 可选尾行 identity)。返回绝对路径。
|
|
83
|
-
*
|
|
84
|
-
* 文件名用 `<ts>_<sessionId>.jsonl` 格式(满足 extractSessionIdFromFilename)。传 rootSessionId
|
|
85
|
-
* 时追加尾行 subagent-identity custom entry(P-fallback fixture 用);不传则只有 header(manifest 主 fixture 用)。
|
|
86
|
-
*/
|
|
87
|
-
async function makeSubagentSession(
|
|
88
|
-
dir: string,
|
|
89
|
-
opts: {
|
|
90
|
-
name: string
|
|
91
|
-
id: string
|
|
92
|
-
cwd?: string
|
|
93
|
-
rootSessionId?: string
|
|
94
|
-
slug?: string
|
|
95
|
-
task?: string
|
|
96
|
-
agent?: string
|
|
97
|
-
firstUserText?: string
|
|
98
|
-
},
|
|
99
|
-
): Promise<string> {
|
|
100
|
-
const header: Record<string, unknown> = {
|
|
101
|
-
type: 'session',
|
|
102
|
-
id: opts.id,
|
|
103
|
-
timestamp: '2026-01-01T00:00:00.000Z',
|
|
104
|
-
}
|
|
105
|
-
if (opts.cwd) header.cwd = opts.cwd
|
|
106
|
-
const lines: string[] = [JSON.stringify(header)]
|
|
107
|
-
if (opts.firstUserText) {
|
|
108
|
-
lines.push(
|
|
109
|
-
JSON.stringify({
|
|
110
|
-
type: 'message',
|
|
111
|
-
id: opts.id + '-m1',
|
|
112
|
-
message: { role: 'user', content: [{ type: 'text', text: opts.firstUserText }] },
|
|
113
|
-
}),
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
if (opts.rootSessionId !== undefined) {
|
|
117
|
-
const data: Record<string, unknown> = {
|
|
118
|
-
id: 'sa-' + opts.id,
|
|
119
|
-
agent: opts.agent ?? 'worker',
|
|
120
|
-
mode: 'background',
|
|
121
|
-
task: opts.task ?? '',
|
|
122
|
-
slug: opts.slug ?? '',
|
|
123
|
-
startedAt: Date.now(),
|
|
124
|
-
rootSessionId: opts.rootSessionId,
|
|
125
|
-
depth: 1,
|
|
126
|
-
}
|
|
127
|
-
lines.push(
|
|
128
|
-
JSON.stringify({ type: 'custom', id: opts.id, customType: 'subagent-identity', data }),
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
await mkdir(dir, { recursive: true })
|
|
132
|
-
const path = join(dir, opts.name)
|
|
133
|
-
await writeFile(path, lines.join('\n') + '\n')
|
|
134
|
-
return path
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
describe('findSessions', () => {
|
|
138
|
-
let root: string
|
|
139
|
-
let agentDir: string
|
|
140
|
-
let slugDir: string
|
|
141
|
-
/**
|
|
142
|
-
* subagent fixture 目录([session-store-layout-inversion] 新布局存储树
|
|
143
|
-
* `<root>/<rootSessionId>/subagents/sessions/`,resolveSessionRoots [subagent] 根
|
|
144
|
-
* 定向扫描路径;分区键取 FIX_SID,生产同源推导函数落盘)。
|
|
145
|
-
*/
|
|
146
|
-
let saDir: string
|
|
147
|
-
|
|
148
|
-
let recordsDir: string
|
|
149
|
-
|
|
150
|
-
/** 存储树分区 sid fixture(合法 pi sid 形态)。 */
|
|
151
|
-
const FIX_SID = '0aaaaaaa-bbbb-7ccc-dddd-0000000000f1'
|
|
152
|
-
|
|
153
|
-
beforeEach(async () => {
|
|
154
|
-
// 生产拓扑复刻(布局 B):agentDir = <dataDir>/agent
|
|
155
|
-
root = await mkdtemp(join(tmpdir(), 'find-test-'))
|
|
156
|
-
agentDir = join(root, 'agent')
|
|
157
|
-
slugDir = join(agentDir, 'sessions', '--Users-demo--')
|
|
158
|
-
saDir = resolveStoreSubagentSessionsDir(agentDir, FIX_SID)
|
|
159
|
-
recordsDir = resolveStoreSubagentRecordsDir(agentDir, FIX_SID)
|
|
160
|
-
})
|
|
161
|
-
afterEach(async () => {
|
|
162
|
-
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('uuid 片段匹配正确的 session(sessionId 含 query)', async () => {
|
|
166
|
-
await makeSession(slugDir, { name: 'a.jsonl', id: '019e6c96-aaaa-bbbb', cwd: '/demo' })
|
|
167
|
-
await makeSession(slugDir, { name: 'b.jsonl', id: '019fffff-cccc-dddd', cwd: '/demo' })
|
|
168
|
-
|
|
169
|
-
const { matches } = await findSessions('e6c96', agentDir)
|
|
170
|
-
expect(matches).toHaveLength(1)
|
|
171
|
-
expect(matches[0].sessionId).toBe('019e6c96-aaaa-bbbb')
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('uuid 片段匹配:文件路径含 query(query 出现在文件名)', async () => {
|
|
175
|
-
// sessionId 不含 query,但文件名含 → path.includes(query) 命中
|
|
176
|
-
await makeSession(slugDir, { name: '2026-special-name.jsonl', id: 'sid-no-query', cwd: '/demo' })
|
|
177
|
-
const { matches } = await findSessions('special', agentDir)
|
|
178
|
-
expect(matches).toHaveLength(1)
|
|
179
|
-
expect(matches[0].sessionId).toBe('sid-no-query')
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('fileName 填完整绝对路径,mtime/sizeBytes/cwd 为真实值', async () => {
|
|
183
|
-
const path = await makeSession(slugDir, { name: 'a.jsonl', id: 'sid-fullpath', cwd: '/demo' })
|
|
184
|
-
const { matches } = await findSessions('sid-fullpath', agentDir)
|
|
185
|
-
expect(matches).toHaveLength(1)
|
|
186
|
-
expect(matches[0].fileName).toBe(path)
|
|
187
|
-
expect(matches[0].mtime).toBeGreaterThan(0)
|
|
188
|
-
expect(matches[0].sizeBytes).toBeGreaterThan(0)
|
|
189
|
-
expect(matches[0].cwd).toBe('/demo')
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('cwd 过滤:只留 header.cwd === opts.cwd 的', async () => {
|
|
193
|
-
await makeSession(slugDir, { name: 'a.jsonl', id: 'aaa-shared', cwd: '/proj-a' })
|
|
194
|
-
await makeSession(slugDir, { name: 'b.jsonl', id: 'bbb-shared', cwd: '/proj-b' })
|
|
195
|
-
|
|
196
|
-
// query 'shared' 同时命中两个 id,cwd 过滤后只留 /proj-a
|
|
197
|
-
const { matches } = await findSessions('shared', agentDir, { cwd: '/proj-a' })
|
|
198
|
-
expect(matches).toHaveLength(1)
|
|
199
|
-
expect(matches[0].sessionId).toBe('aaa-shared')
|
|
200
|
-
expect(matches[0].cwd).toBe('/proj-a')
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('缺 cwd 的 header 进候选集(谓词不统一负面锚,design §4 负面行为验证)', async () => {
|
|
204
|
-
// header 行只有 id(无 cwd/timestamp):runtime 导入侧「id+cwd 非空」严谓词会拒收,
|
|
205
|
-
// reader 宽谓词(仅要求 type==='session' ∧ id 为 string)必须照常收录——基座化
|
|
206
|
-
// (readFirstJsonlLine 改指 session-core)不得顺手统一谓词,本用例即机器锚。
|
|
207
|
-
const headerPath = join(slugDir, 'no-cwd.jsonl')
|
|
208
|
-
await mkdir(slugDir, { recursive: true })
|
|
209
|
-
await writeFile(headerPath, JSON.stringify({ type: 'session', id: '019e0001-no-cwd' }) + '\n')
|
|
210
|
-
await makeSession(slugDir, { name: 'with-cwd.jsonl', id: '019e0002-with-cwd', cwd: '/demo' })
|
|
211
|
-
|
|
212
|
-
// find 结果:缺 cwd header 命中,cwd 归一化为空串(buildCandidate 的 header.cwd ?? '')
|
|
213
|
-
const { matches } = await findSessions('no-cwd', agentDir)
|
|
214
|
-
expect(matches).toHaveLength(1)
|
|
215
|
-
expect(matches[0].sessionId).toBe('019e0001-no-cwd')
|
|
216
|
-
expect(matches[0].cwd).toBe('')
|
|
217
|
-
|
|
218
|
-
// 候选枚举(recent 路径枚举全部候选):缺 cwd header 未被候选集排除
|
|
219
|
-
const recent = await findSessions('recent', agentDir)
|
|
220
|
-
expect(recent.matches.map((m) => m.sessionId).sort()).toEqual([
|
|
221
|
-
'019e0001-no-cwd',
|
|
222
|
-
'019e0002-with-cwd',
|
|
223
|
-
])
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
it('limit 截断:truncated 标记正确', async () => {
|
|
227
|
-
// 5 个文件 id 都含 "common",uuid 片段 "common" 全匹配
|
|
228
|
-
for (let i = 0; i < 5; i++) {
|
|
229
|
-
await makeSession(slugDir, { name: `f${i}.jsonl`, id: `common-${i}`, cwd: '/demo' })
|
|
230
|
-
}
|
|
231
|
-
const { matches, truncated } = await findSessions('common', agentDir, { limit: 2 })
|
|
232
|
-
expect(matches).toHaveLength(2)
|
|
233
|
-
expect(truncated).toBe(true)
|
|
234
|
-
|
|
235
|
-
// limit >= 总数时 truncated=false
|
|
236
|
-
const all = await findSessions('common', agentDir, { limit: 10 })
|
|
237
|
-
expect(all.matches).toHaveLength(5)
|
|
238
|
-
expect(all.truncated).toBe(false)
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
it("query='recent' 按 mtime 倒序", async () => {
|
|
242
|
-
const paths: string[] = []
|
|
243
|
-
for (let i = 0; i < 3; i++) {
|
|
244
|
-
paths.push(await makeSession(slugDir, { name: `r${i}.jsonl`, id: `recent-${i}`, cwd: '/demo' }))
|
|
245
|
-
}
|
|
246
|
-
// 设递增 mtime:r0 最旧,r2 最新
|
|
247
|
-
const base = Math.floor(Date.now() / 1000)
|
|
248
|
-
await utimes(paths[0], base, base)
|
|
249
|
-
await utimes(paths[1], base + 100, base + 100)
|
|
250
|
-
await utimes(paths[2], base + 200, base + 200)
|
|
251
|
-
|
|
252
|
-
const { matches } = await findSessions('recent', agentDir)
|
|
253
|
-
expect(matches).toHaveLength(3)
|
|
254
|
-
expect(matches[0].sessionId).toBe('recent-2') // mtime 最大排前
|
|
255
|
-
expect(matches[1].sessionId).toBe('recent-1')
|
|
256
|
-
expect(matches[2].sessionId).toBe('recent-0')
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it("recent 也尊重 cwd 过滤", async () => {
|
|
260
|
-
await makeSession(slugDir, { name: 'a.jsonl', id: 'r-a', cwd: '/proj-a' })
|
|
261
|
-
await makeSession(slugDir, { name: 'b.jsonl', id: 'r-b', cwd: '/proj-b' })
|
|
262
|
-
|
|
263
|
-
const { matches } = await findSessions('recent', agentDir, { cwd: '/proj-a' })
|
|
264
|
-
expect(matches).toHaveLength(1)
|
|
265
|
-
expect(matches[0].sessionId).toBe('r-a')
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
it('名称关键词匹配:uuid 无匹配时读首消息预览含 query', async () => {
|
|
269
|
-
// query 'plugin' 含 p/l/u/g/i/n(p/l/u 非十六进制)→ 非 uuid 特征,走首消息 fallback
|
|
270
|
-
await makeSession(slugDir, {
|
|
271
|
-
name: 'a.jsonl',
|
|
272
|
-
id: 'kw-aaaa',
|
|
273
|
-
cwd: '/demo',
|
|
274
|
-
firstUserText: '重构插件架构 plugin architecture review',
|
|
275
|
-
})
|
|
276
|
-
await makeSession(slugDir, {
|
|
277
|
-
name: 'b.jsonl',
|
|
278
|
-
id: 'kw-bbbb',
|
|
279
|
-
cwd: '/demo',
|
|
280
|
-
firstUserText: '完全无关的内容 unrelated content',
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
const { matches } = await findSessions('plugin', agentDir)
|
|
284
|
-
expect(matches).toHaveLength(1)
|
|
285
|
-
expect(matches[0].sessionId).toBe('kw-aaaa')
|
|
286
|
-
expect(matches[0].firstMessagePreview).toContain('plugin')
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
it('无匹配返回 { matches: [], truncated: false }', async () => {
|
|
290
|
-
await makeSession(slugDir, { name: 'a.jsonl', id: '019abcd', cwd: '/demo' })
|
|
291
|
-
// query 非 uuid 特征(z 非十六进制),走首消息 fallback 仍无匹配
|
|
292
|
-
const result = await findSessions('zzznotexist', agentDir)
|
|
293
|
-
expect(result).toEqual({ matches: [], truncated: false })
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
it('uuid 片段命中路径的 firstMessagePreview 补读(命中时未读首消息,第 5 步补)', async () => {
|
|
297
|
-
// uuid 片段直接命中 → 不走关键词层(preview 未读),最终结果仍应补读出首消息预览
|
|
298
|
-
await makeSession(slugDir, {
|
|
299
|
-
name: 'a.jsonl',
|
|
300
|
-
id: '019cafe0-aaaa-bbbb',
|
|
301
|
-
cwd: '/demo',
|
|
302
|
-
firstUserText: 'uuid 命中后补读的预览文本',
|
|
303
|
-
})
|
|
304
|
-
const { matches } = await findSessions('019cafe0', agentDir)
|
|
305
|
-
expect(matches).toHaveLength(1)
|
|
306
|
-
expect(matches[0].sessionId).toBe('019cafe0-aaaa-bbbb')
|
|
307
|
-
expect(matches[0].firstMessagePreview).toBe('uuid 命中后补读的预览文本')
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
it('firstMessagePreview 截断到 80 字符', async () => {
|
|
311
|
-
const longText = 'X'.repeat(200)
|
|
312
|
-
await makeSession(slugDir, {
|
|
313
|
-
name: 'a.jsonl',
|
|
314
|
-
id: 'prev-len',
|
|
315
|
-
cwd: '/demo',
|
|
316
|
-
firstUserText: longText,
|
|
317
|
-
})
|
|
318
|
-
const { matches } = await findSessions('prev-len', agentDir)
|
|
319
|
-
expect(matches).toHaveLength(1)
|
|
320
|
-
expect(matches[0].firstMessagePreview).toBeDefined()
|
|
321
|
-
expect(matches[0].firstMessagePreview!.length).toBeLessThanOrEqual(80)
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
it('纯十六进制 query 无 uuid 匹配时不走首消息 fallback(uuid 特征短路)', async () => {
|
|
325
|
-
// 'deadbeef' 全十六进制 → uuid 特征;sessionId 不含、首消息也不会含 → matched=[]
|
|
326
|
-
await makeSession(slugDir, {
|
|
327
|
-
name: 'a.jsonl',
|
|
328
|
-
id: '019aaaa',
|
|
329
|
-
cwd: '/demo',
|
|
330
|
-
firstUserText: 'deadbeef 出现在首消息里也不该匹配',
|
|
331
|
-
})
|
|
332
|
-
const result = await findSessions('deadbeef', agentDir)
|
|
333
|
-
expect(result.matches).toEqual([])
|
|
334
|
-
expect(result.truncated).toBe(false)
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
it('缺省合并:main + subagent 候选都返回,source 标记正确(DM1)', async () => {
|
|
338
|
-
await makeSession(slugDir, { name: 'm.jsonl', id: 'main-shared', cwd: '/demo' })
|
|
339
|
-
await makeSession(saDir, { name: 's.jsonl', id: 'sub-shared', cwd: '/demo' })
|
|
340
|
-
|
|
341
|
-
const { matches } = await findSessions('shared', agentDir)
|
|
342
|
-
expect(matches).toHaveLength(2)
|
|
343
|
-
// source 必填标记(DM1)+ 集合为 {main, subagent}
|
|
344
|
-
const sources = matches.map((m) => m.source).sort()
|
|
345
|
-
expect(sources).toEqual(['main', 'subagent'])
|
|
346
|
-
// fileName 指向各自目录
|
|
347
|
-
const mainHit = matches.find((m) => m.source === 'main')!
|
|
348
|
-
const subHit = matches.find((m) => m.source === 'subagent')!
|
|
349
|
-
expect(mainHit.sessionId).toBe('main-shared')
|
|
350
|
-
expect(mainHit.fileName).not.toContain('subagents')
|
|
351
|
-
expect(subHit.sessionId).toBe('sub-shared')
|
|
352
|
-
expect(subHit.fileName).toContain('subagents')
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
it("source:'main' 只含 main 候选(subagent 被过滤)", async () => {
|
|
356
|
-
await makeSession(slugDir, { name: 'm.jsonl', id: 'main-shared', cwd: '/demo' })
|
|
357
|
-
await makeSession(saDir, { name: 's.jsonl', id: 'sub-shared', cwd: '/demo' })
|
|
358
|
-
|
|
359
|
-
const { matches } = await findSessions('shared', agentDir, { source: 'main' })
|
|
360
|
-
expect(matches).toHaveLength(1)
|
|
361
|
-
expect(matches[0].source).toBe('main')
|
|
362
|
-
expect(matches[0].sessionId).toBe('main-shared')
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
it("source:'subagent' 只含 subagent 候选(main 被过滤)", async () => {
|
|
366
|
-
await makeSession(slugDir, { name: 'm.jsonl', id: 'main-shared', cwd: '/demo' })
|
|
367
|
-
await makeSession(saDir, { name: 's.jsonl', id: 'sub-shared', cwd: '/demo' })
|
|
368
|
-
|
|
369
|
-
const { matches } = await findSessions('shared', agentDir, { source: 'subagent' })
|
|
370
|
-
expect(matches).toHaveLength(1)
|
|
371
|
-
expect(matches[0].source).toBe('subagent')
|
|
372
|
-
expect(matches[0].sessionId).toBe('sub-shared')
|
|
373
|
-
expect(matches[0].fileName).toContain('subagents')
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
it('D6 单新根锚定:旧布局 <agentDir>/subagents 造 subagent 会话 → find 不发现(存量不可见)', async () => {
|
|
377
|
-
await makeSession(slugDir, { name: 'm.jsonl', id: 'main-shared', cwd: '/demo' })
|
|
378
|
-
// 旧布局存量原位保留(D6 裁决:不迁移、不再读写)
|
|
379
|
-
const legacyDir = join(agentDir, 'subagents', '--Users-demo--', 'sessions')
|
|
380
|
-
await makeSession(legacyDir, { name: 's-legacy.jsonl', id: 'sub-legacy', cwd: '/demo' })
|
|
381
|
-
|
|
382
|
-
const { matches } = await findSessions('shared', agentDir)
|
|
383
|
-
expect(matches.map((m) => m.sessionId)).toEqual(['main-shared'])
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
it('存储树分区目录名(= rootSessionId)不把根会话 id 的完整 id 查询吸成伪 multi', async () => {
|
|
387
|
-
// 新布局路径 `<root>/<rootSessionId>/subagents/sessions/<subId>.jsonl` 含根会话 id——
|
|
388
|
-
// 完整 id 查询若走路径子串匹配会同时命中 subagent 候选(伪 multi 消歧)。
|
|
389
|
-
// 精确等值短路:完整 id 直查恒精确命中(find.ts [session-store-layout-inversion])。
|
|
390
|
-
const rootId = '0aaaaaaa-bbbb-7ccc-dddd-0000000000q1'
|
|
391
|
-
const subId = '0aaaaaaa-bbbb-7ccc-dddd-0000000000q2'
|
|
392
|
-
await makeSession(slugDir, { name: `${rootId}.jsonl`, id: rootId, cwd: '/demo' })
|
|
393
|
-
await makeSession(resolveStoreSubagentSessionsDir(agentDir, rootId), {
|
|
394
|
-
name: `${subId}.jsonl`,
|
|
395
|
-
id: subId,
|
|
396
|
-
cwd: '/demo',
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
// 完整 id:精确等值直返,不被 subagent 路径里的分区 id 误吸
|
|
400
|
-
const exact = await findSessions(rootId, agentDir)
|
|
401
|
-
expect(exact.matches.map((m) => m.sessionId)).toEqual([rootId])
|
|
402
|
-
// 真片段查询:路径维度命中仍然保留(按路径片段定位文件的能力不回退)
|
|
403
|
-
const fragment = await findSessions('0000000000q', agentDir)
|
|
404
|
-
expect(fragment.matches.map((m) => m.sessionId).sort()).toEqual([rootId, subId].sort())
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
it("recent 也尊重 source 过滤(含不传 source 时两侧按 mtime 混合倒序)", async () => {
|
|
408
|
-
const mainPath = await makeSession(slugDir, { name: 'm.jsonl', id: 'r-main', cwd: '/demo' })
|
|
409
|
-
const subPath = await makeSession(saDir, { name: 's.jsonl', id: 'r-sub', cwd: '/demo' })
|
|
410
|
-
// subagent mtime 更新
|
|
411
|
-
const base = Math.floor(Date.now() / 1000)
|
|
412
|
-
await utimes(mainPath, base, base)
|
|
413
|
-
await utimes(subPath, base + 100, base + 100)
|
|
414
|
-
|
|
415
|
-
// source:'main' → 只 main 侧,subagent 被过滤
|
|
416
|
-
const mainOnly = await findSessions('recent', agentDir, { source: 'main' })
|
|
417
|
-
expect(mainOnly.matches).toHaveLength(1)
|
|
418
|
-
expect(mainOnly.matches[0].source).toBe('main')
|
|
419
|
-
expect(mainOnly.matches[0].sessionId).toBe('r-main')
|
|
420
|
-
|
|
421
|
-
// 不传 source → 两侧合并按 mtime 倒序(subagent 更新排前)
|
|
422
|
-
const merged = await findSessions('recent', agentDir)
|
|
423
|
-
expect(merged.matches).toHaveLength(2)
|
|
424
|
-
expect(merged.matches[0].sessionId).toBe('r-sub')
|
|
425
|
-
expect(merged.matches[0].source).toBe('subagent')
|
|
426
|
-
expect(merged.matches[1].sessionId).toBe('r-main')
|
|
427
|
-
expect(merged.matches[1].source).toBe('main')
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
// ============================================================
|
|
431
|
-
// U5:subagent task/slug/agentName 匹配(manifest 索引 + P-fallback identity 回退)
|
|
432
|
-
// ============================================================
|
|
433
|
-
describe('U5 task/slug/agentName 匹配', () => {
|
|
434
|
-
it('TC-u5-find-task:manifest.task 含 query 入选(main 首消息不含 query 不入选)', async () => {
|
|
435
|
-
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000001'
|
|
436
|
-
const subPath = await makeSubagentSession(saDir, {
|
|
437
|
-
name: `1234567890_${subId}.jsonl`,
|
|
438
|
-
id: subId,
|
|
439
|
-
cwd: '/demo',
|
|
440
|
-
})
|
|
441
|
-
await makeRecordManifest(recordsDir, {
|
|
442
|
-
id: `sa-${subId}`,
|
|
443
|
-
rootSessionId: 'root-1',
|
|
444
|
-
sessionFile: subPath,
|
|
445
|
-
task: '调研 codex CLI 的功能',
|
|
446
|
-
})
|
|
447
|
-
// main 首消息不含 codex → 关键词层不命中
|
|
448
|
-
await makeSession(slugDir, {
|
|
449
|
-
name: 'main.jsonl',
|
|
450
|
-
id: 'main-no-task-match',
|
|
451
|
-
cwd: '/demo',
|
|
452
|
-
firstUserText: '完全无关的对话内容',
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
const { matches } = await findSessions('codex', agentDir)
|
|
456
|
-
expect(matches).toHaveLength(1)
|
|
457
|
-
expect(matches[0].source).toBe('subagent')
|
|
458
|
-
expect(matches[0].sessionId).toBe(subId)
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
it('TC-u5-find-slug:manifest.slug 子串命中入选', async () => {
|
|
462
|
-
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000002'
|
|
463
|
-
const subPath = await makeSubagentSession(saDir, {
|
|
464
|
-
name: `1234567890_${subId}.jsonl`,
|
|
465
|
-
id: subId,
|
|
466
|
-
cwd: '/demo',
|
|
467
|
-
})
|
|
468
|
-
await makeRecordManifest(recordsDir, {
|
|
469
|
-
id: `sa-${subId}`,
|
|
470
|
-
rootSessionId: 'root-2',
|
|
471
|
-
sessionFile: subPath,
|
|
472
|
-
slug: 'codex-ask-user-research',
|
|
473
|
-
task: '其他不含 ask-user 的任务文本',
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
const { matches } = await findSessions('ask-user', agentDir)
|
|
477
|
-
expect(matches).toHaveLength(1)
|
|
478
|
-
expect(matches[0].source).toBe('subagent')
|
|
479
|
-
expect(matches[0].sessionId).toBe(subId)
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
it('TC-u5-find-agentname:manifest.agentName 命中入选', async () => {
|
|
483
|
-
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000003'
|
|
484
|
-
const subPath = await makeSubagentSession(saDir, {
|
|
485
|
-
name: `1234567890_${subId}.jsonl`,
|
|
486
|
-
id: subId,
|
|
487
|
-
cwd: '/demo',
|
|
488
|
-
})
|
|
489
|
-
await makeRecordManifest(recordsDir, {
|
|
490
|
-
id: `sa-${subId}`,
|
|
491
|
-
rootSessionId: 'root-3',
|
|
492
|
-
sessionFile: subPath,
|
|
493
|
-
agentName: 'explorer',
|
|
494
|
-
task: '不含 explorer 的任务',
|
|
495
|
-
slug: '不含-explorer-的-slug',
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
const { matches } = await findSessions('explorer', agentDir)
|
|
499
|
-
expect(matches).toHaveLength(1)
|
|
500
|
-
expect(matches[0].source).toBe('subagent')
|
|
501
|
-
expect(matches[0].sessionId).toBe(subId)
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
it('TC-u5-find-source-task-combo:source:subagent 过滤 + task 匹配正交(与 m0 U1 source 过滤组合)', async () => {
|
|
505
|
-
// main:首消息含 codex,但 source:'subagent' 在文件列表层排除 sessions/ 目录(不扫 main)
|
|
506
|
-
await makeSession(slugDir, {
|
|
507
|
-
name: 'main.jsonl',
|
|
508
|
-
id: 'main-with-codex',
|
|
509
|
-
cwd: '/demo',
|
|
510
|
-
firstUserText: '讨论 codex 工具的使用',
|
|
511
|
-
})
|
|
512
|
-
// subagent:task 含 codex
|
|
513
|
-
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000004'
|
|
514
|
-
const subPath = await makeSubagentSession(saDir, {
|
|
515
|
-
name: `1234567890_${subId}.jsonl`,
|
|
516
|
-
id: subId,
|
|
517
|
-
cwd: '/demo',
|
|
518
|
-
})
|
|
519
|
-
await makeRecordManifest(recordsDir, {
|
|
520
|
-
id: `sa-${subId}`,
|
|
521
|
-
rootSessionId: 'root-4',
|
|
522
|
-
sessionFile: subPath,
|
|
523
|
-
task: '用 codex 完成任务',
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
const { matches } = await findSessions('codex', agentDir, { source: 'subagent' })
|
|
527
|
-
expect(matches.length).toBeGreaterThanOrEqual(1)
|
|
528
|
-
expect(matches.every((m) => m.source === 'subagent')).toBe(true)
|
|
529
|
-
expect(matches.some((m) => m.sessionId === subId)).toBe(true)
|
|
530
|
-
// main 被 source 过滤排除(文件列表层不扫 sessions/)
|
|
531
|
-
expect(matches.some((m) => m.source === 'main')).toBe(false)
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
it('TC-u5-find-pfallback:无 manifest,读尾行 identity.task 回退匹配(场景 A)', async () => {
|
|
535
|
-
// subagent:无 manifest(P-fallback),但 session 文件尾行 identity.task 含 'resolve-bug'
|
|
536
|
-
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000005'
|
|
537
|
-
await makeSubagentSession(saDir, {
|
|
538
|
-
name: `1234567890_${subId}.jsonl`,
|
|
539
|
-
id: subId,
|
|
540
|
-
cwd: '/demo',
|
|
541
|
-
rootSessionId: 'root-5',
|
|
542
|
-
task: '修复 resolve-bug 这个问题',
|
|
543
|
-
slug: 'fix',
|
|
544
|
-
agent: 'worker',
|
|
545
|
-
})
|
|
546
|
-
|
|
547
|
-
const { matches } = await findSessions('resolve', agentDir)
|
|
548
|
-
expect(matches).toHaveLength(1)
|
|
549
|
-
expect(matches[0].source).toBe('subagent')
|
|
550
|
-
expect(matches[0].sessionId).toBe(subId)
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
it('TC-u5-find-uuid-priority:uuid 片段命中后短路,不走 task 匹配(TC-find-match-priority)', async () => {
|
|
554
|
-
// subagent A:sessionId 含 'abc123'(uuid 片段命中)
|
|
555
|
-
const idA = 'aabc123e-0000-0000-0000-000000000001'
|
|
556
|
-
await makeSubagentSession(saDir, {
|
|
557
|
-
name: `1111111111_${idA}.jsonl`,
|
|
558
|
-
id: idA,
|
|
559
|
-
cwd: '/demo',
|
|
560
|
-
})
|
|
561
|
-
// subagent B:sessionId 不含 abc123,但 manifest.task 含 abc123
|
|
562
|
-
const idB = '0aaaaaaa-bbbb-cccc-dddd-000000000006'
|
|
563
|
-
const subPathB = await makeSubagentSession(saDir, {
|
|
564
|
-
name: `2222222222_${idB}.jsonl`,
|
|
565
|
-
id: idB,
|
|
566
|
-
cwd: '/demo',
|
|
567
|
-
})
|
|
568
|
-
await makeRecordManifest(recordsDir, {
|
|
569
|
-
id: `sa-${idB}`,
|
|
570
|
-
rootSessionId: 'root-6',
|
|
571
|
-
sessionFile: subPathB,
|
|
572
|
-
task: '任务包含 abc123 关键词',
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
// 'abc123' 全十六进制 → uuid 特征;A 的 sessionId 含 abc123 → uuid 片段命中 → 短路
|
|
576
|
-
const { matches } = await findSessions('abc123', agentDir)
|
|
577
|
-
expect(matches).toHaveLength(1)
|
|
578
|
-
expect(matches[0].sessionId).toBe(idA)
|
|
579
|
-
// B 不入选(uuid 命中短路,不走 task 匹配)
|
|
580
|
-
expect(matches.some((m) => m.sessionId === idB)).toBe(false)
|
|
581
|
-
})
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
it('uuid 片段命中候选补全元数据:fileName/mtime/sizeBytes 真实值(019e6c96 锚)', async () => {
|
|
585
|
-
// 2 条 session、恰 1 条 id 含片段 'e6c96':uuid 片段路径命中后仍补读元数据字段。
|
|
586
|
-
// 命中文件按真实 pi 落盘命名(<id>.jsonl),fileName 断言含该 id
|
|
587
|
-
await makeSession(slugDir, { name: `${FIX_E6}.jsonl`, id: FIX_E6, cwd: '/demo' })
|
|
588
|
-
await makeSession(slugDir, { name: 'other.jsonl', id: '019ffff-cccc-dddd', cwd: '/demo' })
|
|
589
|
-
|
|
590
|
-
const { matches } = await findSessions('e6c96', agentDir)
|
|
591
|
-
expect(matches.length).toBeGreaterThan(0)
|
|
592
|
-
const hit = matches.find((m) => m.sessionId.startsWith('019e6c96'))!
|
|
593
|
-
expect(hit.fileName).toContain('019e6c96')
|
|
594
|
-
expect(hit.mtime).toBeGreaterThan(0)
|
|
595
|
-
expect(hit.sizeBytes).toBeGreaterThan(0)
|
|
596
|
-
})
|
|
597
|
-
})
|
|
598
|
-
|
|
599
|
-
// ============================================================
|
|
600
|
-
// u11:标题元数据 metadataProvider 注入(design 2026-09-10 §6.6)
|
|
601
|
-
// 覆盖三条调用策略中的 ①惰性 ②窄化(TTL 缓存句柄在 tool-handler 注入侧,
|
|
602
|
-
// 见 tool-handler.test.ts u11 段)+ 两条 guard + §11.3b 多目录合并语义
|
|
603
|
-
// + 纯 TS 首条 user 降级路径保留。
|
|
604
|
-
// provider 一律注入替身(不真调 pi SessionManager)。
|
|
605
|
-
// ============================================================
|
|
606
|
-
describe('u11 标题元数据(metadataProvider 注入,§6.6)', () => {
|
|
607
|
-
let root: string
|
|
608
|
-
let agentDir: string
|
|
609
|
-
/** 平铺 main 根(taiji 形态:session 直在 sessions/ 下,[default] 根无子目录) */
|
|
610
|
-
let flatMain: string
|
|
611
|
-
/** legacy 根(dirname(agentDir)/sessions) */
|
|
612
|
-
let legacyDir: string
|
|
613
|
-
|
|
614
|
-
async function writeSession(
|
|
615
|
-
dir: string,
|
|
616
|
-
opts: { name: string; id: string; cwd?: string; firstUserText?: string },
|
|
617
|
-
): Promise<string> {
|
|
618
|
-
await mkdir(dir, { recursive: true })
|
|
619
|
-
const lines = [JSON.stringify({ type: 'session', id: opts.id, cwd: opts.cwd ?? '/demo' })]
|
|
620
|
-
if (opts.firstUserText !== undefined) {
|
|
621
|
-
lines.push(
|
|
622
|
-
JSON.stringify({
|
|
623
|
-
type: 'message',
|
|
624
|
-
id: `${opts.id}-m1`,
|
|
625
|
-
message: { role: 'user', content: [{ type: 'text', text: opts.firstUserText }] },
|
|
626
|
-
}),
|
|
627
|
-
)
|
|
628
|
-
}
|
|
629
|
-
const path = join(dir, opts.name)
|
|
630
|
-
await writeFile(path, lines.join('\n') + '\n')
|
|
631
|
-
return path
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
beforeEach(async () => {
|
|
635
|
-
// agentDir 挂在 mkdtemp 子目录下,legacy 根(dirname(agentDir)/sessions)仍在
|
|
636
|
-
// 自建 tmp 树内,不污染共享 tmpdir(也不受其他用例残留影响)
|
|
637
|
-
root = await mkdtemp(join(tmpdir(), 'find-u11-'))
|
|
638
|
-
agentDir = join(root, 'agent')
|
|
639
|
-
flatMain = join(agentDir, 'sessions')
|
|
640
|
-
legacyDir = join(root, 'sessions')
|
|
641
|
-
})
|
|
642
|
-
afterEach(async () => {
|
|
643
|
-
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
644
|
-
})
|
|
645
|
-
|
|
646
|
-
it('① 惰性触发:uuid 精确/归一化路径不调 provider(spy 计数 0)', async () => {
|
|
647
|
-
await writeSession(flatMain, { name: 'a.jsonl', id: '019cafe00001', firstUserText: '无关' })
|
|
648
|
-
const provider = vi.fn(async () => [])
|
|
649
|
-
// 精确子串层命中
|
|
650
|
-
await findSessions('019cafe00001', agentDir, { metadataProvider: provider })
|
|
651
|
-
expect(provider).not.toHaveBeenCalled()
|
|
652
|
-
// 归一化层命中(大写变体:精确层大小写敏感 0 命中,norm 层命中后短路)
|
|
653
|
-
await findSessions('019CAFE00001', agentDir, { metadataProvider: provider })
|
|
654
|
-
expect(provider).not.toHaveBeenCalled()
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
it('② keyword 路径调用 provider 且标题进匹配(name 命中,preview 取元数据首消息免深读)', async () => {
|
|
658
|
-
const pathA = await writeSession(flatMain, {
|
|
659
|
-
name: 'a.jsonl',
|
|
660
|
-
id: 'title-aaaa',
|
|
661
|
-
firstUserText: '首消息完全无关',
|
|
662
|
-
})
|
|
663
|
-
await writeSession(flatMain, { name: 'b.jsonl', id: 'title-bbbb', firstUserText: '另一个无关会话' })
|
|
664
|
-
const provider = vi.fn(async (dir: string) =>
|
|
665
|
-
dir === flatMain
|
|
666
|
-
? [
|
|
667
|
-
{
|
|
668
|
-
path: pathA,
|
|
669
|
-
id: 'title-aaaa',
|
|
670
|
-
cwd: '/demo',
|
|
671
|
-
name: '福耀玻璃深度研究',
|
|
672
|
-
modified: new Date('2026-09-01'),
|
|
673
|
-
firstMessage: '首消息完全无关',
|
|
674
|
-
},
|
|
675
|
-
]
|
|
676
|
-
: [],
|
|
677
|
-
)
|
|
678
|
-
const { matches } = await findSessions('福耀玻璃', agentDir, { metadataProvider: provider })
|
|
679
|
-
// 平铺存在的 [default] 根被调用,且永传非空串
|
|
680
|
-
expect(provider).toHaveBeenCalledTimes(1)
|
|
681
|
-
expect(provider.mock.calls[0][0]).toBe(flatMain)
|
|
682
|
-
expect(provider.mock.calls[0][0].length).toBeGreaterThan(0)
|
|
683
|
-
// 标题命中:name 透传 + preview 取元数据 firstMessage
|
|
684
|
-
expect(matches).toHaveLength(1)
|
|
685
|
-
expect(matches[0].sessionId).toBe('title-aaaa')
|
|
686
|
-
expect(matches[0].name).toBe('福耀玻璃深度研究')
|
|
687
|
-
expect(matches[0].firstMessagePreview).toBe('首消息完全无关')
|
|
688
|
-
})
|
|
689
|
-
|
|
690
|
-
it('③ 窄化:含子目录根跳过 provider,候选不退出 keyword 匹配(回退首条 user,标题留空)', async () => {
|
|
691
|
-
// 纯 pi 形态:文件在 encodeCwd 子目录 → [default] 根含子目录 → listAll 跳过
|
|
692
|
-
await writeSession(join(flatMain, '--Users-demo--'), {
|
|
693
|
-
name: 'a.jsonl',
|
|
694
|
-
id: 'nested-aaaa',
|
|
695
|
-
firstUserText: '重构插件架构 plugin review',
|
|
696
|
-
})
|
|
697
|
-
const provider = vi.fn(async () => [])
|
|
698
|
-
const { matches } = await findSessions('plugin', agentDir, { metadataProvider: provider })
|
|
699
|
-
expect(provider).not.toHaveBeenCalled()
|
|
700
|
-
expect(matches).toHaveLength(1)
|
|
701
|
-
expect(matches[0].sessionId).toBe('nested-aaaa')
|
|
702
|
-
expect(matches[0].firstMessagePreview).toContain('plugin')
|
|
703
|
-
// 标题维度降级留空:「能找到 session」不受影响(§6.6 降级路径)
|
|
704
|
-
expect(matches[0].name).toBeUndefined()
|
|
705
|
-
})
|
|
706
|
-
|
|
707
|
-
it('④ guard:provider 抛错降级(标题留空 + 不抛,回退首条 user 匹配)', async () => {
|
|
708
|
-
await writeSession(flatMain, {
|
|
709
|
-
name: 'a.jsonl',
|
|
710
|
-
id: 'err-aaaa',
|
|
711
|
-
firstUserText: '按首条消息命中的文本',
|
|
712
|
-
})
|
|
713
|
-
const provider = vi.fn(async () => {
|
|
714
|
-
throw new Error('listAll boom')
|
|
715
|
-
})
|
|
716
|
-
const { matches } = await findSessions('按首条消息命中', agentDir, {
|
|
717
|
-
metadataProvider: provider,
|
|
718
|
-
})
|
|
719
|
-
expect(provider).toHaveBeenCalledTimes(1) // 调用了但抛错
|
|
720
|
-
expect(matches).toHaveLength(1)
|
|
721
|
-
expect(matches[0].sessionId).toBe('err-aaaa')
|
|
722
|
-
expect(matches[0].name).toBeUndefined()
|
|
723
|
-
expect(matches[0].firstMessagePreview).toContain('按首条消息命中')
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
it('⑤ 多目录结果合并:按 id 去重、重复取新 modified(§11.3b)', async () => {
|
|
727
|
-
const pathA = await writeSession(flatMain, {
|
|
728
|
-
name: 'a.jsonl',
|
|
729
|
-
id: 'dup-id-0001',
|
|
730
|
-
firstUserText: '无关',
|
|
731
|
-
})
|
|
732
|
-
// legacy 根平铺存在(0 文件也算存在根,isFlatRoot 空集恒真)
|
|
733
|
-
await mkdir(legacyDir, { recursive: true })
|
|
734
|
-
const provider = vi.fn(async (dir: string) =>
|
|
735
|
-
dir === flatMain
|
|
736
|
-
? [{ path: pathA, id: 'dup-id-0001', cwd: '/demo', name: '旧标题', modified: new Date(1000) }]
|
|
737
|
-
: [{ path: pathA, id: 'dup-id-0001', cwd: '/demo', name: '新标题', modified: new Date(2000) }],
|
|
738
|
-
)
|
|
739
|
-
const { matches } = await findSessions('新标题', agentDir, { metadataProvider: provider })
|
|
740
|
-
expect(provider).toHaveBeenCalledTimes(2) // 两个平铺存在的根各一次
|
|
741
|
-
expect(matches).toHaveLength(1)
|
|
742
|
-
expect(matches[0].name).toBe('新标题') // 同 id 重复 → 取新 modified 的条目
|
|
743
|
-
const miss = await findSessions('旧标题', agentDir, { metadataProvider: provider })
|
|
744
|
-
expect(miss.matches).toHaveLength(0) // 旧条目已按合并规则丢弃
|
|
745
|
-
})
|
|
746
|
-
|
|
747
|
-
it('⑥ guard:仅对存在的根调用且永传非空串(不存在根 / 不存在 liveSessionDir 不调)', async () => {
|
|
748
|
-
await writeSession(flatMain, { name: 'a.jsonl', id: 'guard-aaaa', firstUserText: '无关' })
|
|
749
|
-
const provider = vi.fn(async () => [])
|
|
750
|
-
await findSessions('zzznohit9q', agentDir, {
|
|
751
|
-
liveSessionDir: join(agentDir, 'no-such-dir'),
|
|
752
|
-
metadataProvider: provider,
|
|
753
|
-
})
|
|
754
|
-
// 仅平铺存在的 [default] 根被调;不存在的 liveSessionDir 与 [legacy] 根均被排除
|
|
755
|
-
expect(provider).toHaveBeenCalledTimes(1)
|
|
756
|
-
expect(provider.mock.calls[0][0]).toBe(flatMain)
|
|
757
|
-
expect(provider.mock.calls.every((c) => typeof c[0] === 'string' && c[0].length > 0)).toBe(true)
|
|
758
|
-
})
|
|
759
|
-
|
|
760
|
-
it('⑥b liveSessionDir 与平铺根同字面路径 → 同目录只调一次(字面路径去重)', async () => {
|
|
761
|
-
await writeSession(flatMain, { name: 'a.jsonl', id: 'dedup-aaaa', firstUserText: '无关' })
|
|
762
|
-
const provider = vi.fn(async () => [])
|
|
763
|
-
await findSessions('zzznohit9q', agentDir, {
|
|
764
|
-
liveSessionDir: flatMain,
|
|
765
|
-
metadataProvider: provider,
|
|
766
|
-
})
|
|
767
|
-
expect(provider).toHaveBeenCalledTimes(1)
|
|
768
|
-
expect(provider.mock.calls[0][0]).toBe(flatMain)
|
|
769
|
-
})
|
|
770
|
-
|
|
771
|
-
it('⑦ recent:仅对 limit 截断后的少数候选补元数据(旧候选所在根不调)', async () => {
|
|
772
|
-
const pNew1 = await writeSession(flatMain, { name: 'n1.jsonl', id: 'recent-new-1' })
|
|
773
|
-
const pNew2 = await writeSession(flatMain, { name: 'n2.jsonl', id: 'recent-new-2' })
|
|
774
|
-
await writeSession(legacyDir, { name: 'o1.jsonl', id: 'recent-old-1' })
|
|
775
|
-
await writeSession(legacyDir, { name: 'o2.jsonl', id: 'recent-old-2' })
|
|
776
|
-
const base = Math.floor(Date.now() / 1000)
|
|
777
|
-
await utimes(pNew1, base + 300, base + 300)
|
|
778
|
-
await utimes(pNew2, base + 200, base + 200)
|
|
779
|
-
await utimes(join(legacyDir, 'o1.jsonl'), base + 100, base + 100)
|
|
780
|
-
await utimes(join(legacyDir, 'o2.jsonl'), base + 50, base + 50)
|
|
781
|
-
const provider = vi.fn(async (dir: string) =>
|
|
782
|
-
dir === flatMain
|
|
783
|
-
? [
|
|
784
|
-
{ path: pNew1, id: 'recent-new-1', cwd: '/demo', name: '最新标题一', modified: new Date((base + 300) * 1000) },
|
|
785
|
-
{ path: pNew2, id: 'recent-new-2', cwd: '/demo', name: '最新标题二', modified: new Date((base + 200) * 1000) },
|
|
786
|
-
]
|
|
787
|
-
: [],
|
|
788
|
-
)
|
|
789
|
-
const { matches, truncated } = await findSessions('recent', agentDir, {
|
|
790
|
-
limit: 2,
|
|
791
|
-
metadataProvider: provider,
|
|
792
|
-
})
|
|
793
|
-
expect(truncated).toBe(true)
|
|
794
|
-
expect(matches.map((m) => m.sessionId)).toEqual(['recent-new-1', 'recent-new-2'])
|
|
795
|
-
// provider 只调最新两个候选所在的平铺根;被截断的 legacy 根候选不触发补元数据
|
|
796
|
-
expect(provider).toHaveBeenCalledTimes(1)
|
|
797
|
-
expect(provider.mock.calls[0][0]).toBe(flatMain)
|
|
798
|
-
// 截断后少数候选补到标题
|
|
799
|
-
expect(matches[0].name).toBe('最新标题一')
|
|
800
|
-
expect(matches[1].name).toBe('最新标题二')
|
|
801
|
-
})
|
|
802
|
-
})
|