@zhushanwen/pi-session-reader 0.1.0 → 0.2.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 +1 -1
- package/src/__tests__/execution-tree.test.ts +792 -0
- package/src/__tests__/family.test.ts +150 -0
- package/src/__tests__/find.test.ts +342 -1
- package/src/__tests__/index.test.ts +50 -0
- package/src/__tests__/subagents.test.ts +291 -14
- package/src/__tests__/tool-handler.test.ts +862 -3
- package/src/__tests__/tree.test.ts +20 -0
- package/src/__tests__/workflow.test.ts +282 -0
- package/src/core/execution-tree.ts +517 -0
- package/src/core/family.ts +40 -0
- package/src/core/tree.ts +14 -5
- package/src/core/workflow.ts +329 -0
- package/src/discovery/find.ts +167 -20
- package/src/discovery/subagents.ts +113 -152
- package/src/discovery/workflows.ts +162 -0
- package/src/index.ts +22 -4
- package/src/tool-handler.ts +334 -21
|
@@ -6,6 +6,7 @@ import type { Entry } from '../core/parser.js'
|
|
|
6
6
|
const ROOT = '019fe620' // 家族根(无 parentSession)
|
|
7
7
|
const FORK = '019fe632' // fork 子代(parentSession 指向 ROOT 的文件)
|
|
8
8
|
const SUB = '019fe635' // subagent(rootSessionId=FORK,挂在 fork 子代下,非家族根)
|
|
9
|
+
const SUB2 = '019fe636' // U4 第二个 subagent(多 subagent 场景)
|
|
9
10
|
|
|
10
11
|
// ---- fixture helpers ----
|
|
11
12
|
|
|
@@ -32,6 +33,31 @@ function subagentIdentity(id: string, rootSessionId: string, slug: string): Entr
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/** U4 富字段(可选,模拟 manifest 主/P-fallback identity 回退组装的 data) */
|
|
37
|
+
interface SubagentRichFields {
|
|
38
|
+
task?: string
|
|
39
|
+
agent?: string
|
|
40
|
+
model?: string
|
|
41
|
+
status?: string
|
|
42
|
+
sessionFile?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 构造带 U4 富字段的 subagent-identity custom entry(manifest 主/P-fallback 两种 data 形态) */
|
|
46
|
+
function subagentIdentityRich(
|
|
47
|
+
id: string,
|
|
48
|
+
rootSessionId: string,
|
|
49
|
+
slug: string,
|
|
50
|
+
rich?: SubagentRichFields,
|
|
51
|
+
): Entry {
|
|
52
|
+
const data: Record<string, unknown> = { rootSessionId, slug }
|
|
53
|
+
if (rich?.task !== undefined) data.task = rich.task
|
|
54
|
+
if (rich?.agent !== undefined) data.agent = rich.agent
|
|
55
|
+
if (rich?.model !== undefined) data.model = rich.model
|
|
56
|
+
if (rich?.status !== undefined) data.status = rich.status
|
|
57
|
+
if (rich?.sessionFile !== undefined) data.sessionFile = rich.sessionFile
|
|
58
|
+
return { type: 'custom', id, parentId: null, customType: 'subagent-identity', data }
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
/** 构造 fileStats Map(M1 key=sessionId) */
|
|
36
62
|
function makeStats(
|
|
37
63
|
keys: string[],
|
|
@@ -205,3 +231,127 @@ describe('resolveFamily - 错误', () => {
|
|
|
205
231
|
expect(() => resolveFamily('unknown-session', index)).toThrow(/not found in family index/)
|
|
206
232
|
})
|
|
207
233
|
})
|
|
234
|
+
|
|
235
|
+
// ============================================================
|
|
236
|
+
// U4: SubagentRef 富字段(manifest 主 / P-fallback identity 回退)
|
|
237
|
+
// 验证 buildFamilyIndex 从 identity entry.data 读 task/agent/model/status/sessionFile
|
|
238
|
+
// 填 SubagentRef,异名映射(data.agent → agentName),守卫放宽(只校验 rootSessionId+slug)。
|
|
239
|
+
// ============================================================
|
|
240
|
+
|
|
241
|
+
describe('U4: SubagentRef 富字段(buildFamilyIndex 透传)', () => {
|
|
242
|
+
it('TC-u4-manifest-enrich: manifest 主路径,富字段全透传到 SubagentRef', () => {
|
|
243
|
+
const identities = [
|
|
244
|
+
subagentIdentityRich(SUB, ROOT, 'codex-research', {
|
|
245
|
+
task: '调研 codex',
|
|
246
|
+
agent: 'explorer',
|
|
247
|
+
model: 'glm-5.2',
|
|
248
|
+
status: 'completed',
|
|
249
|
+
sessionFile: '/path/to/sub.jsonl',
|
|
250
|
+
}),
|
|
251
|
+
]
|
|
252
|
+
const index = buildFamilyIndex([header(ROOT)], identities, makeStats([ROOT, SUB]))
|
|
253
|
+
|
|
254
|
+
const sub = resolveFamily(ROOT, index).subagents.find((s) => s.sessionId === SUB)
|
|
255
|
+
expect(sub?.task).toBe('调研 codex')
|
|
256
|
+
expect(sub?.slug).toBe('codex-research')
|
|
257
|
+
// 异名映射核心断言:identity.data.agent → SubagentRef.agentName
|
|
258
|
+
expect(sub?.agentName).toBe('explorer')
|
|
259
|
+
expect(sub?.model).toBe('glm-5.2')
|
|
260
|
+
expect(sub?.status).toBe('completed')
|
|
261
|
+
expect(sub?.sessionFile).toBe('/path/to/sub.jsonl')
|
|
262
|
+
expect(sub?.cleanedUp).toBe(false)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('TC-u4-pfallback-identity: P-fallback(有 identity 无 model/status),task/agent/sessionFile 透传', () => {
|
|
266
|
+
const identities = [
|
|
267
|
+
subagentIdentityRich(SUB, ROOT, 'fix', {
|
|
268
|
+
task: 'fix bug',
|
|
269
|
+
agent: 'worker',
|
|
270
|
+
sessionFile: '/alive/sub.jsonl',
|
|
271
|
+
// 无 model/status(P-fallback identity 不含这两项,探针 15/15 确认)
|
|
272
|
+
}),
|
|
273
|
+
]
|
|
274
|
+
const index = buildFamilyIndex([header(ROOT)], identities, makeStats([ROOT, SUB]))
|
|
275
|
+
|
|
276
|
+
const sub = resolveFamily(ROOT, index).subagents.find((s) => s.sessionId === SUB)
|
|
277
|
+
expect(sub?.task).toBe('fix bug')
|
|
278
|
+
expect(sub?.slug).toBe('fix')
|
|
279
|
+
expect(sub?.agentName).toBe('worker')
|
|
280
|
+
expect(sub?.sessionFile).toBe('/alive/sub.jsonl')
|
|
281
|
+
// P-fallback 核心断言:model/status 必 undefined(identity 不可回退)
|
|
282
|
+
expect(sub?.model).toBeUndefined()
|
|
283
|
+
expect(sub?.status).toBeUndefined()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('TC-u4-pfallback-no-identity: data 缺 rootSessionId → 守卫拒掉,不入 subagentsByRoot,不抛错', () => {
|
|
287
|
+
// 模拟无 identity(运行中/异常,尾行是 message 非 identity):data 只有 slug 无 rootSessionId
|
|
288
|
+
const noRoot: Entry = {
|
|
289
|
+
type: 'custom',
|
|
290
|
+
id: 'no-root',
|
|
291
|
+
parentId: null,
|
|
292
|
+
customType: 'subagent-identity',
|
|
293
|
+
data: { slug: 'dangling', task: 't' }, // 缺 rootSessionId
|
|
294
|
+
}
|
|
295
|
+
const index = buildFamilyIndex([header(ROOT)], [noRoot], makeStats([ROOT]))
|
|
296
|
+
|
|
297
|
+
// 守卫拒掉 → 不入 subagentsByRoot
|
|
298
|
+
expect(index.subagentsByRoot.size).toBe(0)
|
|
299
|
+
// resolveFamily 不抛错,subagents 空
|
|
300
|
+
const family = resolveFamily(ROOT, index)
|
|
301
|
+
expect(family.subagents).toHaveLength(0)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('TC-u4-orphan-manifest: fileStats 不含 id → cleanedUp=true,富字段仍透传(GC 路径保留)', () => {
|
|
305
|
+
const identities = [
|
|
306
|
+
subagentIdentityRich('sa-ghost', ROOT, 'ghost-slug', {
|
|
307
|
+
task: 'ghost task',
|
|
308
|
+
agent: 'worker',
|
|
309
|
+
model: 'gpt-4',
|
|
310
|
+
status: 'completed',
|
|
311
|
+
sessionFile: '/gc/ghost.jsonl',
|
|
312
|
+
}),
|
|
313
|
+
]
|
|
314
|
+
// fileStats 不含 'sa-ghost'(模拟 .jsonl 被 GC,manifest 残留)→ cleanedUp=true
|
|
315
|
+
const index = buildFamilyIndex([header(ROOT)], identities, makeStats([ROOT]))
|
|
316
|
+
|
|
317
|
+
const sub = resolveFamily(ROOT, index).subagents.find((s) => s.sessionId === 'sa-ghost')
|
|
318
|
+
expect(sub?.cleanedUp).toBe(true)
|
|
319
|
+
expect(sub?.task).toBe('ghost task')
|
|
320
|
+
expect(sub?.agentName).toBe('worker')
|
|
321
|
+
expect(sub?.model).toBe('gpt-4')
|
|
322
|
+
expect(sub?.status).toBe('completed')
|
|
323
|
+
expect(sub?.sessionFile).toBe('/gc/ghost.jsonl') // GC 路径保留(不置空)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('TC-u4-recordmanifest-compat: 最小 identity(仅 rootSessionId+slug)→ 富字段全 undefined,不抛错', () => {
|
|
327
|
+
// 模拟旧 manifest(无 task/slug/model/status/agentName)经 buildFamilyFromFs 转成的最小 identity
|
|
328
|
+
const identities = [subagentIdentity(SUB, ROOT, 'legacy')]
|
|
329
|
+
const index = buildFamilyIndex([header(ROOT)], identities, makeStats([ROOT, SUB]))
|
|
330
|
+
|
|
331
|
+
const sub = resolveFamily(ROOT, index).subagents.find((s) => s.sessionId === SUB)
|
|
332
|
+
expect(sub?.slug).toBe('legacy')
|
|
333
|
+
expect(sub?.rootSessionId).toBe(ROOT)
|
|
334
|
+
expect(sub?.task).toBeUndefined()
|
|
335
|
+
expect(sub?.agentName).toBeUndefined()
|
|
336
|
+
expect(sub?.model).toBeUndefined()
|
|
337
|
+
expect(sub?.status).toBeUndefined()
|
|
338
|
+
expect(sub?.sessionFile).toBeUndefined()
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('isSubagentIdentityData 守卫放宽:富字段部分缺失/全缺都通过(只校验 rootSessionId+slug)', () => {
|
|
342
|
+
const identities = [
|
|
343
|
+
subagentIdentityRich(SUB, ROOT, 's1', { task: 'only-task' }), // 部分富字段
|
|
344
|
+
subagentIdentity(SUB2, ROOT, 's2'), // 完全无富字段(旧 manifest 形态)
|
|
345
|
+
]
|
|
346
|
+
const index = buildFamilyIndex([header(ROOT)], identities, makeStats([ROOT, SUB, SUB2]))
|
|
347
|
+
|
|
348
|
+
const family = resolveFamily(ROOT, index)
|
|
349
|
+
expect(family.subagents).toHaveLength(2)
|
|
350
|
+
const s1 = family.subagents.find((s) => s.sessionId === SUB)
|
|
351
|
+
expect(s1?.task).toBe('only-task')
|
|
352
|
+
expect(s1?.model).toBeUndefined()
|
|
353
|
+
const s2 = family.subagents.find((s) => s.sessionId === SUB2)
|
|
354
|
+
expect(s2?.task).toBeUndefined()
|
|
355
|
+
expect(s2?.agentName).toBeUndefined()
|
|
356
|
+
})
|
|
357
|
+
})
|
|
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
|
|
|
3
3
|
import { mkdtemp, mkdir, writeFile, rm, utimes } from 'node:fs/promises'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { findSessions } from '../discovery/find.js'
|
|
6
|
-
import { REAL_AGENT_DIR, HAS_E6 } from './real-data.js'
|
|
6
|
+
import { REAL_AGENT_DIR, HAS_E6, HAS_REAL_SUBAGENTS_DIR } from './real-data.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* 建一个假 session 文件:首行 header(type=session,含 id/cwd/parentSession),
|
|
@@ -42,13 +42,107 @@ async function makeSession(
|
|
|
42
42
|
return path
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 建 records manifest(U5 fixture)。manifest 在 subagent 创建时写入 records/<sa-id>.json,
|
|
47
|
+
* sessionFile 指向 alive subagent session 的绝对路径(find 用 extractSessionIdFromFilename
|
|
48
|
+
* 从该路径提取 sessionId 建索引)。
|
|
49
|
+
*/
|
|
50
|
+
async function makeRecordManifest(
|
|
51
|
+
recordsDir: string,
|
|
52
|
+
opts: {
|
|
53
|
+
id: string
|
|
54
|
+
rootSessionId: string
|
|
55
|
+
sessionFile: string
|
|
56
|
+
agentName?: string
|
|
57
|
+
task?: string
|
|
58
|
+
slug?: string
|
|
59
|
+
model?: string
|
|
60
|
+
status?: string
|
|
61
|
+
},
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const m: Record<string, unknown> = {
|
|
64
|
+
id: opts.id,
|
|
65
|
+
rootSessionId: opts.rootSessionId,
|
|
66
|
+
sessionFile: opts.sessionFile,
|
|
67
|
+
}
|
|
68
|
+
if (opts.agentName !== undefined) m.agentName = opts.agentName
|
|
69
|
+
if (opts.task !== undefined) m.task = opts.task
|
|
70
|
+
if (opts.slug !== undefined) m.slug = opts.slug
|
|
71
|
+
if (opts.model !== undefined) m.model = opts.model
|
|
72
|
+
if (opts.status !== undefined) m.status = opts.status
|
|
73
|
+
await mkdir(recordsDir, { recursive: true })
|
|
74
|
+
await writeFile(join(recordsDir, `${opts.id}.json`), JSON.stringify(m))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 建 subagent session 文件(header + 可选首消息 + 可选尾行 identity)。返回绝对路径。
|
|
79
|
+
*
|
|
80
|
+
* 文件名用 `<ts>_<sessionId>.jsonl` 格式(满足 extractSessionIdFromFilename)。传 rootSessionId
|
|
81
|
+
* 时追加尾行 subagent-identity custom entry(P-fallback fixture 用);不传则只有 header(manifest 主 fixture 用)。
|
|
82
|
+
*/
|
|
83
|
+
async function makeSubagentSession(
|
|
84
|
+
dir: string,
|
|
85
|
+
opts: {
|
|
86
|
+
name: string
|
|
87
|
+
id: string
|
|
88
|
+
cwd?: string
|
|
89
|
+
rootSessionId?: string
|
|
90
|
+
slug?: string
|
|
91
|
+
task?: string
|
|
92
|
+
agent?: string
|
|
93
|
+
firstUserText?: string
|
|
94
|
+
},
|
|
95
|
+
): Promise<string> {
|
|
96
|
+
const header: Record<string, unknown> = {
|
|
97
|
+
type: 'session',
|
|
98
|
+
id: opts.id,
|
|
99
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
100
|
+
}
|
|
101
|
+
if (opts.cwd) header.cwd = opts.cwd
|
|
102
|
+
const lines: string[] = [JSON.stringify(header)]
|
|
103
|
+
if (opts.firstUserText) {
|
|
104
|
+
lines.push(
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
type: 'message',
|
|
107
|
+
id: opts.id + '-m1',
|
|
108
|
+
message: { role: 'user', content: [{ type: 'text', text: opts.firstUserText }] },
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
if (opts.rootSessionId !== undefined) {
|
|
113
|
+
const data: Record<string, unknown> = {
|
|
114
|
+
id: 'sa-' + opts.id,
|
|
115
|
+
agent: opts.agent ?? 'worker',
|
|
116
|
+
mode: 'background',
|
|
117
|
+
task: opts.task ?? '',
|
|
118
|
+
slug: opts.slug ?? '',
|
|
119
|
+
startedAt: Date.now(),
|
|
120
|
+
rootSessionId: opts.rootSessionId,
|
|
121
|
+
depth: 1,
|
|
122
|
+
}
|
|
123
|
+
lines.push(
|
|
124
|
+
JSON.stringify({ type: 'custom', id: opts.id, customType: 'subagent-identity', data }),
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
await mkdir(dir, { recursive: true })
|
|
128
|
+
const path = join(dir, opts.name)
|
|
129
|
+
await writeFile(path, lines.join('\n') + '\n')
|
|
130
|
+
return path
|
|
131
|
+
}
|
|
132
|
+
|
|
45
133
|
describe('findSessions', () => {
|
|
46
134
|
let agentDir: string
|
|
47
135
|
let slugDir: string
|
|
136
|
+
/** subagent fixture 目录(模拟 subagents/<cwd编码>/sessions/ 结构,roots.listSubagentSessions 扫描路径) */
|
|
137
|
+
let saDir: string
|
|
138
|
+
|
|
139
|
+
let recordsDir: string
|
|
48
140
|
|
|
49
141
|
beforeEach(async () => {
|
|
50
142
|
agentDir = await mkdtemp(join(tmpdir(), 'find-test-'))
|
|
51
143
|
slugDir = join(agentDir, 'sessions', '--Users-demo--')
|
|
144
|
+
saDir = join(agentDir, 'subagents', '--Users-demo--', 'sessions')
|
|
145
|
+
recordsDir = join(agentDir, 'subagents', '--Users-demo--', 'records')
|
|
52
146
|
})
|
|
53
147
|
afterEach(async () => {
|
|
54
148
|
await rm(agentDir, { recursive: true, force: true })
|
|
@@ -189,6 +283,236 @@ describe('findSessions', () => {
|
|
|
189
283
|
expect(result.truncated).toBe(false)
|
|
190
284
|
})
|
|
191
285
|
|
|
286
|
+
it('缺省合并:main + subagent 候选都返回,source 标记正确(DM1)', async () => {
|
|
287
|
+
await makeSession(slugDir, { name: 'm.jsonl', id: 'main-shared', cwd: '/demo' })
|
|
288
|
+
await makeSession(saDir, { name: 's.jsonl', id: 'sub-shared', cwd: '/demo' })
|
|
289
|
+
|
|
290
|
+
const { matches } = await findSessions('shared', agentDir)
|
|
291
|
+
expect(matches).toHaveLength(2)
|
|
292
|
+
// source 必填标记(DM1)+ 集合为 {main, subagent}
|
|
293
|
+
const sources = matches.map((m) => m.source).sort()
|
|
294
|
+
expect(sources).toEqual(['main', 'subagent'])
|
|
295
|
+
// fileName 指向各自目录
|
|
296
|
+
const mainHit = matches.find((m) => m.source === 'main')!
|
|
297
|
+
const subHit = matches.find((m) => m.source === 'subagent')!
|
|
298
|
+
expect(mainHit.sessionId).toBe('main-shared')
|
|
299
|
+
expect(mainHit.fileName).not.toContain('subagents')
|
|
300
|
+
expect(subHit.sessionId).toBe('sub-shared')
|
|
301
|
+
expect(subHit.fileName).toContain('subagents')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it("source:'main' 只含 main 候选(subagent 被过滤)", async () => {
|
|
305
|
+
await makeSession(slugDir, { name: 'm.jsonl', id: 'main-shared', cwd: '/demo' })
|
|
306
|
+
await makeSession(saDir, { name: 's.jsonl', id: 'sub-shared', cwd: '/demo' })
|
|
307
|
+
|
|
308
|
+
const { matches } = await findSessions('shared', agentDir, { source: 'main' })
|
|
309
|
+
expect(matches).toHaveLength(1)
|
|
310
|
+
expect(matches[0].source).toBe('main')
|
|
311
|
+
expect(matches[0].sessionId).toBe('main-shared')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it("source:'subagent' 只含 subagent 候选(main 被过滤)", async () => {
|
|
315
|
+
await makeSession(slugDir, { name: 'm.jsonl', id: 'main-shared', cwd: '/demo' })
|
|
316
|
+
await makeSession(saDir, { name: 's.jsonl', id: 'sub-shared', cwd: '/demo' })
|
|
317
|
+
|
|
318
|
+
const { matches } = await findSessions('shared', agentDir, { source: 'subagent' })
|
|
319
|
+
expect(matches).toHaveLength(1)
|
|
320
|
+
expect(matches[0].source).toBe('subagent')
|
|
321
|
+
expect(matches[0].sessionId).toBe('sub-shared')
|
|
322
|
+
expect(matches[0].fileName).toContain('subagents')
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it("recent 也尊重 source 过滤(含不传 source 时两侧按 mtime 混合倒序)", async () => {
|
|
326
|
+
const mainPath = await makeSession(slugDir, { name: 'm.jsonl', id: 'r-main', cwd: '/demo' })
|
|
327
|
+
const subPath = await makeSession(saDir, { name: 's.jsonl', id: 'r-sub', cwd: '/demo' })
|
|
328
|
+
// subagent mtime 更新
|
|
329
|
+
const base = Math.floor(Date.now() / 1000)
|
|
330
|
+
await utimes(mainPath, base, base)
|
|
331
|
+
await utimes(subPath, base + 100, base + 100)
|
|
332
|
+
|
|
333
|
+
// source:'main' → 只 main 侧,subagent 被过滤
|
|
334
|
+
const mainOnly = await findSessions('recent', agentDir, { source: 'main' })
|
|
335
|
+
expect(mainOnly.matches).toHaveLength(1)
|
|
336
|
+
expect(mainOnly.matches[0].source).toBe('main')
|
|
337
|
+
expect(mainOnly.matches[0].sessionId).toBe('r-main')
|
|
338
|
+
|
|
339
|
+
// 不传 source → 两侧合并按 mtime 倒序(subagent 更新排前)
|
|
340
|
+
const merged = await findSessions('recent', agentDir)
|
|
341
|
+
expect(merged.matches).toHaveLength(2)
|
|
342
|
+
expect(merged.matches[0].sessionId).toBe('r-sub')
|
|
343
|
+
expect(merged.matches[0].source).toBe('subagent')
|
|
344
|
+
expect(merged.matches[1].sessionId).toBe('r-main')
|
|
345
|
+
expect(merged.matches[1].source).toBe('main')
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// ============================================================
|
|
349
|
+
// U5:subagent task/slug/agentName 匹配(manifest 索引 + P-fallback identity 回退)
|
|
350
|
+
// ============================================================
|
|
351
|
+
describe('U5 task/slug/agentName 匹配', () => {
|
|
352
|
+
it('TC-u5-find-task:manifest.task 含 query 入选(main 首消息不含 query 不入选)', async () => {
|
|
353
|
+
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000001'
|
|
354
|
+
const subPath = await makeSubagentSession(saDir, {
|
|
355
|
+
name: `1234567890_${subId}.jsonl`,
|
|
356
|
+
id: subId,
|
|
357
|
+
cwd: '/demo',
|
|
358
|
+
})
|
|
359
|
+
await makeRecordManifest(recordsDir, {
|
|
360
|
+
id: `sa-${subId}`,
|
|
361
|
+
rootSessionId: 'root-1',
|
|
362
|
+
sessionFile: subPath,
|
|
363
|
+
task: '调研 codex CLI 的功能',
|
|
364
|
+
})
|
|
365
|
+
// main 首消息不含 codex → 关键词层不命中
|
|
366
|
+
await makeSession(slugDir, {
|
|
367
|
+
name: 'main.jsonl',
|
|
368
|
+
id: 'main-no-task-match',
|
|
369
|
+
cwd: '/demo',
|
|
370
|
+
firstUserText: '完全无关的对话内容',
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const { matches } = await findSessions('codex', agentDir)
|
|
374
|
+
expect(matches).toHaveLength(1)
|
|
375
|
+
expect(matches[0].source).toBe('subagent')
|
|
376
|
+
expect(matches[0].sessionId).toBe(subId)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('TC-u5-find-slug:manifest.slug 子串命中入选', async () => {
|
|
380
|
+
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000002'
|
|
381
|
+
const subPath = await makeSubagentSession(saDir, {
|
|
382
|
+
name: `1234567890_${subId}.jsonl`,
|
|
383
|
+
id: subId,
|
|
384
|
+
cwd: '/demo',
|
|
385
|
+
})
|
|
386
|
+
await makeRecordManifest(recordsDir, {
|
|
387
|
+
id: `sa-${subId}`,
|
|
388
|
+
rootSessionId: 'root-2',
|
|
389
|
+
sessionFile: subPath,
|
|
390
|
+
slug: 'codex-ask-user-research',
|
|
391
|
+
task: '其他不含 ask-user 的任务文本',
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
const { matches } = await findSessions('ask-user', agentDir)
|
|
395
|
+
expect(matches).toHaveLength(1)
|
|
396
|
+
expect(matches[0].source).toBe('subagent')
|
|
397
|
+
expect(matches[0].sessionId).toBe(subId)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('TC-u5-find-agentname:manifest.agentName 命中入选', async () => {
|
|
401
|
+
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000003'
|
|
402
|
+
const subPath = await makeSubagentSession(saDir, {
|
|
403
|
+
name: `1234567890_${subId}.jsonl`,
|
|
404
|
+
id: subId,
|
|
405
|
+
cwd: '/demo',
|
|
406
|
+
})
|
|
407
|
+
await makeRecordManifest(recordsDir, {
|
|
408
|
+
id: `sa-${subId}`,
|
|
409
|
+
rootSessionId: 'root-3',
|
|
410
|
+
sessionFile: subPath,
|
|
411
|
+
agentName: 'explorer',
|
|
412
|
+
task: '不含 explorer 的任务',
|
|
413
|
+
slug: '不含-explorer-的-slug',
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
const { matches } = await findSessions('explorer', agentDir)
|
|
417
|
+
expect(matches).toHaveLength(1)
|
|
418
|
+
expect(matches[0].source).toBe('subagent')
|
|
419
|
+
expect(matches[0].sessionId).toBe(subId)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('TC-u5-find-source-task-combo:source:subagent 过滤 + task 匹配正交(与 m0 U1 source 过滤组合)', async () => {
|
|
423
|
+
// main:首消息含 codex,但 source:'subagent' 在文件列表层排除 sessions/ 目录(不扫 main)
|
|
424
|
+
await makeSession(slugDir, {
|
|
425
|
+
name: 'main.jsonl',
|
|
426
|
+
id: 'main-with-codex',
|
|
427
|
+
cwd: '/demo',
|
|
428
|
+
firstUserText: '讨论 codex 工具的使用',
|
|
429
|
+
})
|
|
430
|
+
// subagent:task 含 codex
|
|
431
|
+
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000004'
|
|
432
|
+
const subPath = await makeSubagentSession(saDir, {
|
|
433
|
+
name: `1234567890_${subId}.jsonl`,
|
|
434
|
+
id: subId,
|
|
435
|
+
cwd: '/demo',
|
|
436
|
+
})
|
|
437
|
+
await makeRecordManifest(recordsDir, {
|
|
438
|
+
id: `sa-${subId}`,
|
|
439
|
+
rootSessionId: 'root-4',
|
|
440
|
+
sessionFile: subPath,
|
|
441
|
+
task: '用 codex 完成任务',
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
const { matches } = await findSessions('codex', agentDir, { source: 'subagent' })
|
|
445
|
+
expect(matches.length).toBeGreaterThanOrEqual(1)
|
|
446
|
+
expect(matches.every((m) => m.source === 'subagent')).toBe(true)
|
|
447
|
+
expect(matches.some((m) => m.sessionId === subId)).toBe(true)
|
|
448
|
+
// main 被 source 过滤排除(文件列表层不扫 sessions/)
|
|
449
|
+
expect(matches.some((m) => m.source === 'main')).toBe(false)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('TC-u5-find-pfallback:无 manifest,读尾行 identity.task 回退匹配(场景 A)', async () => {
|
|
453
|
+
// subagent:无 manifest(P-fallback),但 session 文件尾行 identity.task 含 'resolve-bug'
|
|
454
|
+
const subId = '0aaaaaaa-bbbb-cccc-dddd-000000000005'
|
|
455
|
+
await makeSubagentSession(saDir, {
|
|
456
|
+
name: `1234567890_${subId}.jsonl`,
|
|
457
|
+
id: subId,
|
|
458
|
+
cwd: '/demo',
|
|
459
|
+
rootSessionId: 'root-5',
|
|
460
|
+
task: '修复 resolve-bug 这个问题',
|
|
461
|
+
slug: 'fix',
|
|
462
|
+
agent: 'worker',
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const { matches } = await findSessions('resolve', agentDir)
|
|
466
|
+
expect(matches).toHaveLength(1)
|
|
467
|
+
expect(matches[0].source).toBe('subagent')
|
|
468
|
+
expect(matches[0].sessionId).toBe(subId)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('TC-u5-find-uuid-priority:uuid 片段命中后短路,不走 task 匹配(TC-find-match-priority)', async () => {
|
|
472
|
+
// subagent A:sessionId 含 'abc123'(uuid 片段命中)
|
|
473
|
+
const idA = 'aabc123e-0000-0000-0000-000000000001'
|
|
474
|
+
await makeSubagentSession(saDir, {
|
|
475
|
+
name: `1111111111_${idA}.jsonl`,
|
|
476
|
+
id: idA,
|
|
477
|
+
cwd: '/demo',
|
|
478
|
+
})
|
|
479
|
+
// subagent B:sessionId 不含 abc123,但 manifest.task 含 abc123
|
|
480
|
+
const idB = '0aaaaaaa-bbbb-cccc-dddd-000000000006'
|
|
481
|
+
const subPathB = await makeSubagentSession(saDir, {
|
|
482
|
+
name: `2222222222_${idB}.jsonl`,
|
|
483
|
+
id: idB,
|
|
484
|
+
cwd: '/demo',
|
|
485
|
+
})
|
|
486
|
+
await makeRecordManifest(recordsDir, {
|
|
487
|
+
id: `sa-${idB}`,
|
|
488
|
+
rootSessionId: 'root-6',
|
|
489
|
+
sessionFile: subPathB,
|
|
490
|
+
task: '任务包含 abc123 关键词',
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
// 'abc123' 全十六进制 → uuid 特征;A 的 sessionId 含 abc123 → uuid 片段命中 → 短路
|
|
494
|
+
const { matches } = await findSessions('abc123', agentDir)
|
|
495
|
+
expect(matches).toHaveLength(1)
|
|
496
|
+
expect(matches[0].sessionId).toBe(idA)
|
|
497
|
+
// B 不入选(uuid 命中短路,不走 task 匹配)
|
|
498
|
+
expect(matches.some((m) => m.sessionId === idB)).toBe(false)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it.skipIf(!HAS_REAL_SUBAGENTS_DIR)(
|
|
502
|
+
'TC-u5-real-data-guard:find codex source:subagent 命中(本机有 codex 相关 subagent task)',
|
|
503
|
+
async () => {
|
|
504
|
+
const { matches, truncated } = await findSessions('codex', REAL_AGENT_DIR, {
|
|
505
|
+
source: 'subagent',
|
|
506
|
+
limit: 50,
|
|
507
|
+
})
|
|
508
|
+
expect(matches.length).toBeGreaterThan(0)
|
|
509
|
+
expect(matches.every((m) => m.source === 'subagent')).toBe(true)
|
|
510
|
+
expect(typeof truncated).toBe('boolean')
|
|
511
|
+
},
|
|
512
|
+
30000,
|
|
513
|
+
)
|
|
514
|
+
})
|
|
515
|
+
|
|
192
516
|
it.skipIf(!HAS_E6)('真实数据:e6c96 匹配 019e6c96 开头的 session', async () => {
|
|
193
517
|
const { matches } = await findSessions('e6c96', REAL_AGENT_DIR)
|
|
194
518
|
expect(matches.length).toBeGreaterThan(0)
|
|
@@ -211,4 +535,21 @@ describe('findSessions', () => {
|
|
|
211
535
|
// 真实 session 文件远多于 5 → 截断
|
|
212
536
|
expect(truncated).toBe(true)
|
|
213
537
|
}, 30000)
|
|
538
|
+
|
|
539
|
+
it.skipIf(!HAS_REAL_SUBAGENTS_DIR)(
|
|
540
|
+
"真实数据:source:'subagent' 能找到 completed subagent(§7 场景 1 find 部分)",
|
|
541
|
+
async () => {
|
|
542
|
+
const { matches } = await findSessions('recent', REAL_AGENT_DIR, {
|
|
543
|
+
source: 'subagent',
|
|
544
|
+
limit: 5,
|
|
545
|
+
})
|
|
546
|
+
expect(matches.length).toBeGreaterThan(0)
|
|
547
|
+
// 全部 source==='subagent',fileName 在 subagents/ 目录下
|
|
548
|
+
for (const m of matches) {
|
|
549
|
+
expect(m.source).toBe('subagent')
|
|
550
|
+
expect(m.fileName).toContain('subagents')
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
30000,
|
|
554
|
+
)
|
|
214
555
|
})
|
|
@@ -190,4 +190,54 @@ describe('sessionReaderExtension - TypeBox schema 与 SessionReadParams 对齐',
|
|
|
190
190
|
expect(Check(schema, { action: 42 })).toBe(false)
|
|
191
191
|
expect(Check(schema, {})).toBe(false) // 缺必填 action
|
|
192
192
|
})
|
|
193
|
+
|
|
194
|
+
it('source 字段:合法值(main/subagent)通过、非法值拒绝;不传 source 向后兼容', () => {
|
|
195
|
+
const fake = makeFakePi()
|
|
196
|
+
sessionReaderExtension(fake.pi as unknown as ExtensionAPI)
|
|
197
|
+
const toolDef = fake.registerTool.mock.calls[0][0] as { parameters: unknown }
|
|
198
|
+
const schema = toolDef.parameters
|
|
199
|
+
|
|
200
|
+
// 合法值通过(enum 约束)
|
|
201
|
+
expect(Check(schema, { action: 'find', query: 'x', source: 'main' })).toBe(true)
|
|
202
|
+
expect(Check(schema, { action: 'find', query: 'x', source: 'subagent' })).toBe(true)
|
|
203
|
+
// 非法值被拒
|
|
204
|
+
expect(Check(schema, { action: 'find', query: 'x', source: 'bogus' })).toBe(false)
|
|
205
|
+
// 不传 source 向后兼容(既有合法形态仍 true)
|
|
206
|
+
expect(Check(schema, { action: 'find', query: 'x' })).toBe(true)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('TC-w6-schema-action:action enum 含 workflow + runId optional(w6)', () => {
|
|
210
|
+
const fake = makeFakePi()
|
|
211
|
+
sessionReaderExtension(fake.pi as unknown as ExtensionAPI)
|
|
212
|
+
const toolDef = fake.registerTool.mock.calls[0][0] as { parameters: unknown }
|
|
213
|
+
const schema = toolDef.parameters
|
|
214
|
+
|
|
215
|
+
// action='workflow' 不传 runId(合法)
|
|
216
|
+
expect(Check(schema, { action: 'workflow', session: 'e6c96' })).toBe(true)
|
|
217
|
+
// action='workflow' 传 runId(合法)
|
|
218
|
+
expect(Check(schema, { action: 'workflow', session: 'e6c96', runId: 'wf-1' })).toBe(true)
|
|
219
|
+
// runId 传任意 action 均合法(其他 action 忽略 runId,不报错)
|
|
220
|
+
expect(Check(schema, { action: 'find', query: 'x', runId: 'wf-1' })).toBe(true)
|
|
221
|
+
// 不传 runId 向后兼容
|
|
222
|
+
expect(Check(schema, { action: 'outline', session: 'e6c96' })).toBe(true)
|
|
223
|
+
// runId 类型校验:非 string 被拒
|
|
224
|
+
expect(Check(schema, { action: 'workflow', session: 'x', runId: 123 })).toBe(false)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('TC-m3b-schema-recursive:recursive optional boolean(family 专用)', () => {
|
|
228
|
+
const fake = makeFakePi()
|
|
229
|
+
sessionReaderExtension(fake.pi as unknown as ExtensionAPI)
|
|
230
|
+
const toolDef = fake.registerTool.mock.calls[0][0] as { parameters: unknown }
|
|
231
|
+
const schema = toolDef.parameters
|
|
232
|
+
|
|
233
|
+
// recursive=true(合法)
|
|
234
|
+
expect(Check(schema, { action: 'family', session: 'e6c96', recursive: true })).toBe(true)
|
|
235
|
+
// 不传 recursive(向后兼容,合法)
|
|
236
|
+
expect(Check(schema, { action: 'family', session: 'e6c96' })).toBe(true)
|
|
237
|
+
// recursive=false(合法)
|
|
238
|
+
expect(Check(schema, { action: 'family', session: 'e6c96', recursive: false })).toBe(true)
|
|
239
|
+
// recursive 非 boolean 被拒
|
|
240
|
+
expect(Check(schema, { action: 'family', session: 'x', recursive: 'yes' })).toBe(false)
|
|
241
|
+
expect(Check(schema, { action: 'family', session: 'x', recursive: 1 })).toBe(false)
|
|
242
|
+
})
|
|
193
243
|
})
|