@zhushanwen/pi-session-reader 0.7.0 → 0.8.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 +4 -4
- package/src/__tests__/entry-anchor.test.ts +148 -4
- package/src/__tests__/execution-tree.test.ts +6 -12
- package/src/__tests__/find.test.ts +9 -49
- package/src/__tests__/fixtures.ts +15 -0
- package/src/__tests__/parser.test.ts +36 -11
- package/src/__tests__/render.test.ts +22 -18
- package/src/__tests__/roots.test.ts +32 -34
- package/src/__tests__/subagents.test.ts +131 -108
- package/src/__tests__/tool-handler.test.ts +496 -147
- package/src/__tests__/turns.test.ts +21 -19
- package/src/__tests__/workflow.test.ts +319 -23
- package/src/core/workflow.ts +196 -6
- package/src/discovery/entry-anchor.ts +36 -5
- package/src/discovery/roots.ts +1 -2
- package/src/discovery/workflows.ts +232 -22
- package/src/tool-handler.ts +82 -22
- package/src/zcode-anchor-classify.ts +23 -9
- package/src/__tests__/real-data.ts +0 -96
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-session-reader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"taiji": {
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@zhushanwen/pi-extension-logger": "0.6.1",
|
|
20
|
-
"@zhushanwen/pi-ext-guards": "0.4.1",
|
|
21
20
|
"@zhushanwen/session-core": "0.11.0",
|
|
22
|
-
"@zhushanwen/zcode-session-source": "0.2.
|
|
21
|
+
"@zhushanwen/zcode-session-source": "0.2.1",
|
|
22
|
+
"@zhushanwen/pi-ext-guards": "0.4.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@vitest/coverage-v8": "^4.1.9",
|
|
26
26
|
"vitest": "^4.1.8",
|
|
27
|
-
"@zhushanwen/subagent-core": "0.
|
|
27
|
+
"@zhushanwen/subagent-core": "0.12.0"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"index.ts",
|
|
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
|
|
|
3
3
|
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import { SUBAGENT_RECORD_CUSTOM_TYPE } from '@zhushanwen/subagent-core'
|
|
6
|
+
import { SUBAGENT_RECORD_CUSTOM_TYPE, SUBAGENT_RECORD_ENTRY_VERSION } from '@zhushanwen/subagent-core'
|
|
7
7
|
|
|
8
8
|
import { findZcodeEntryAnchor, type ZcodeAnchor } from '../discovery/entry-anchor.js'
|
|
9
9
|
|
|
@@ -53,6 +53,68 @@ function messageLine(entryId: string, text: string): string {
|
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* v2 终态条行(W1 D1:`{v:2, kind:"settled", id, engine?, engineHandle?, ...}`——
|
|
58
|
+
* 载荷形态对齐 subagent-core SubagentRecordSettledEntryData,锚取 engineHandle.sessionRef)。
|
|
59
|
+
*/
|
|
60
|
+
function v2SettledLine(
|
|
61
|
+
entryId: string,
|
|
62
|
+
saId: string,
|
|
63
|
+
opts: {
|
|
64
|
+
sessionRef?: Record<string, string>
|
|
65
|
+
engine?: string
|
|
66
|
+
} = {},
|
|
67
|
+
): string {
|
|
68
|
+
const data: Record<string, unknown> = {
|
|
69
|
+
v: 2,
|
|
70
|
+
kind: 'settled',
|
|
71
|
+
id: saId,
|
|
72
|
+
status: 'idle',
|
|
73
|
+
stopReason: 'end_turn',
|
|
74
|
+
endedAt: 123,
|
|
75
|
+
turns: 1,
|
|
76
|
+
totalTokens: 10,
|
|
77
|
+
model: undefined,
|
|
78
|
+
thinkingLevel: undefined,
|
|
79
|
+
}
|
|
80
|
+
if (opts.engine !== undefined) data.engine = opts.engine
|
|
81
|
+
if (opts.sessionRef !== undefined) {
|
|
82
|
+
data.engineHandle = { sessionRef: opts.sessionRef, poolKey: 'shared' }
|
|
83
|
+
}
|
|
84
|
+
return JSON.stringify({
|
|
85
|
+
type: 'custom',
|
|
86
|
+
customType: 'subagent-record',
|
|
87
|
+
id: entryId,
|
|
88
|
+
parentId: null,
|
|
89
|
+
data,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* v2 注册条行(`{v:2, kind:"registered", id, agent, task, ...}`——身份域字段,不携带
|
|
95
|
+
* engineHandle;设计 D1 裁决「sessionRef 双键取自终态条」,注册条目不是锚源)。
|
|
96
|
+
*/
|
|
97
|
+
function v2RegisteredLine(entryId: string, saId: string): string {
|
|
98
|
+
return JSON.stringify({
|
|
99
|
+
type: 'custom',
|
|
100
|
+
customType: 'subagent-record',
|
|
101
|
+
id: entryId,
|
|
102
|
+
parentId: null,
|
|
103
|
+
data: {
|
|
104
|
+
v: 2,
|
|
105
|
+
kind: 'registered',
|
|
106
|
+
id: saId,
|
|
107
|
+
agent: 'dev',
|
|
108
|
+
task: 't',
|
|
109
|
+
slug: 's',
|
|
110
|
+
origin: 'tool',
|
|
111
|
+
rootSessionId: 'root-1',
|
|
112
|
+
depth: 0,
|
|
113
|
+
startedAt: 1,
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
56
118
|
/** 缺 dbPath 的 sessionRef(pi record 形态:只有 sessionId 单键)。 */
|
|
57
119
|
const REF_NO_DBPATH = { sessionId: 'sess-pi-x' }
|
|
58
120
|
|
|
@@ -146,16 +208,94 @@ describe('形状校验窄而严:缺键条目不算命中,继续找更早条
|
|
|
146
208
|
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
147
209
|
})
|
|
148
210
|
|
|
149
|
-
it('data.v
|
|
211
|
+
it('data.v 为认识的 v1/v2 之外的版本(future-v,如 3)不算命中,更早 v=1 条目仍可命中', async () => {
|
|
150
212
|
const file = writeMainSession('main.jsonl', [
|
|
151
213
|
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
152
214
|
recordLine('e1', 'sa-1', { sessionId: 'sess-A', dbPath: DB_PATH }),
|
|
153
|
-
recordLine('e2', 'sa-1', { sessionId: 'sess-
|
|
215
|
+
recordLine('e2', 'sa-1', { sessionId: 'sess-V3', dbPath: DB_PATH }, 3),
|
|
154
216
|
])
|
|
155
217
|
|
|
156
218
|
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
157
219
|
expect(anchor?.sessionId).toBe('sess-A')
|
|
158
|
-
expect(anchor?.sessionId).not.toBe('sess-
|
|
220
|
+
expect(anchor?.sessionId).not.toBe('sess-V3')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// ============================================================
|
|
225
|
+
// v2 终态条锚定(W1 D1 版本门补齐:sessionRef 双键取自终态条)
|
|
226
|
+
// ============================================================
|
|
227
|
+
|
|
228
|
+
describe('v2 终态条锚定:kind="settled" + engineHandle.sessionRef 双键', () => {
|
|
229
|
+
it('v2 终态条完整锚(sessionRef 双键齐)→ 命中', async () => {
|
|
230
|
+
const file = writeMainSession('main.jsonl', [
|
|
231
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
232
|
+
messageLine('m1', 'dispatch'),
|
|
233
|
+
v2SettledLine('e1', 'sa-1', {
|
|
234
|
+
engine: 'zcode',
|
|
235
|
+
sessionRef: { sessionId: 'sess-V2A', dbPath: DB_PATH },
|
|
236
|
+
}),
|
|
237
|
+
])
|
|
238
|
+
|
|
239
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
240
|
+
expect(anchor).toBeDefined()
|
|
241
|
+
expect(anchor?.sessionId).toBe('sess-V2A')
|
|
242
|
+
expect(anchor?.dbPath).toBe(DB_PATH)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('v2 注册条(kind="registered",不携带 engineHandle)→ 不命中(锚只取自终态条)', async () => {
|
|
246
|
+
const file = writeMainSession('main.jsonl', [
|
|
247
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
248
|
+
v2RegisteredLine('e-reg', 'sa-1'),
|
|
249
|
+
])
|
|
250
|
+
|
|
251
|
+
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('v1 快照条(早)+ v2 终态条(晚)→ 末条 v2 胜(末条锚定跨版本一致)', async () => {
|
|
255
|
+
const file = writeMainSession('main.jsonl', [
|
|
256
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
257
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-V1OLD', dbPath: DB_PATH }),
|
|
258
|
+
v2SettledLine('e2', 'sa-1', {
|
|
259
|
+
engine: 'zcode',
|
|
260
|
+
sessionRef: { sessionId: 'sess-V2NEW', dbPath: DB_PATH },
|
|
261
|
+
}),
|
|
262
|
+
])
|
|
263
|
+
|
|
264
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
265
|
+
expect(anchor?.sessionId).toBe('sess-V2NEW')
|
|
266
|
+
expect(anchor?.sessionId).not.toBe('sess-V1OLD')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('v2 终态条锚残缺(缺 engineHandle)→ 不命中,更早 v1 完整条目仍可命中(缺键不遮蔽)', async () => {
|
|
270
|
+
const file = writeMainSession('main.jsonl', [
|
|
271
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
272
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-EARLY', dbPath: DB_PATH }),
|
|
273
|
+
v2SettledLine('e2', 'sa-1', { engine: 'zcode' }), // 无 sessionRef
|
|
274
|
+
])
|
|
275
|
+
|
|
276
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
277
|
+
expect(anchor?.sessionId).toBe('sess-EARLY')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('v2 但 kind 不在词表(unknown-kind,非 registered/settled)→ 不命中', async () => {
|
|
281
|
+
const badKind = JSON.stringify({
|
|
282
|
+
type: 'custom',
|
|
283
|
+
customType: 'subagent-record',
|
|
284
|
+
id: 'e-bad',
|
|
285
|
+
parentId: null,
|
|
286
|
+
data: {
|
|
287
|
+
v: 2,
|
|
288
|
+
kind: 'migrated',
|
|
289
|
+
id: 'sa-1',
|
|
290
|
+
engineHandle: { sessionRef: { sessionId: 'sess-BAD', dbPath: DB_PATH }, poolKey: 'shared' },
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
const file = writeMainSession('main.jsonl', [
|
|
294
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
295
|
+
badKind,
|
|
296
|
+
])
|
|
297
|
+
|
|
298
|
+
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
159
299
|
})
|
|
160
300
|
})
|
|
161
301
|
|
|
@@ -223,4 +363,8 @@ describe('协议字面量漂移守卫(跨包契约)', () => {
|
|
|
223
363
|
// 源码本地持有磁盘协议字符串(生产依赖面不引 subagent-core),此处锚定两侧同值
|
|
224
364
|
expect(SUBAGENT_RECORD_CUSTOM_TYPE).toBe('subagent-record')
|
|
225
365
|
})
|
|
366
|
+
|
|
367
|
+
it('entry-anchor 本地 v2 认识版本 === subagent-core 写侧单源(写侧再升版本本守卫先红)', () => {
|
|
368
|
+
expect(SUBAGENT_RECORD_ENTRY_VERSION).toBe(2)
|
|
369
|
+
})
|
|
226
370
|
})
|
|
@@ -17,11 +17,9 @@ import { buildExecutionTree, formatExecutionTreeText, type ExecutionTreeNode } f
|
|
|
17
17
|
* - TC-m3b-cycle-detection:workflow 指针环(A→B→A),visited Set 防环
|
|
18
18
|
* - TC-m3b-single-node:单节点树(无后代,ES5)
|
|
19
19
|
* - TC-m3b-source-priority:parentRecordId 三级数据源优先级(manifest>identity>flat,DM4)
|
|
20
|
-
* - TC-m3b-real-data-guard:真实形态数据守卫(合成 fixture
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* 新旧机制混合 / 孤儿 manifest / 坏 manifest / 外来家族),guard 语义不变——解析不抛错 /
|
|
24
|
-
* 树非空 / 结构自洽,不锁 sourceMode 具体值
|
|
20
|
+
* - TC-m3b-real-data-guard:真实形态数据守卫(合成 fixture):mkdtemp 临时目录仿真真实
|
|
21
|
+
* agentDir 布局(时间戳前缀文件名 / 新旧机制混合 / 孤儿 manifest / 坏 manifest / 外来
|
|
22
|
+
* 家族),guard 语义断言——解析不抛错 / 树非空 / 结构自洽,不锁 sourceMode 具体值
|
|
25
23
|
*/
|
|
26
24
|
|
|
27
25
|
// ---- fixture 常量(uuid 特征,互不为子串,满足 extractSessionIdFromFilename)----
|
|
@@ -685,13 +683,9 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
685
683
|
})
|
|
686
684
|
|
|
687
685
|
// ============================================================
|
|
688
|
-
// 真实形态数据守卫(合成 fixture
|
|
686
|
+
// 真实形态数据守卫(合成 fixture):mkdtemp 临时目录仿真真实 agentDir 布局,
|
|
687
|
+
// guard 语义断言(解析不抛错 / 树非空 / 结构自洽 / sourceMode 值域),不触碰真实数据目录。
|
|
689
688
|
// ============================================================
|
|
690
|
-
//
|
|
691
|
-
// 原用例直读本机 ~/.pi/agent(execSync 探测 + findRealTreeRoot 扫真实 records 动态发现
|
|
692
|
-
// 家族根,skipIf CI 无数据):真实数据演化后确定性红(impl-plan 台账登记根因),且触碰
|
|
693
|
-
// 真实数据目录违反 TEST-STRATEGY 红线。数据面合成化——mkdtemp 临时目录仿真真实 agentDir
|
|
694
|
-
// 布局,guard 语义断言(解析不抛错 / 树非空 / 结构自洽 / sourceMode 值域)原样保留。
|
|
695
689
|
|
|
696
690
|
describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
|
|
697
691
|
let dir: string
|
|
@@ -704,7 +698,7 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
|
|
|
704
698
|
})
|
|
705
699
|
|
|
706
700
|
it('TC-m3b-real-data-guard:真实形态布局(时间戳文件名/新旧机制混合/孤儿 manifest/坏 manifest/外来家族)解析不抛错,树非空且结构自洽', async () => {
|
|
707
|
-
// 仿真真实 agentDir
|
|
701
|
+
// 仿真真实 agentDir 数据形态(全部 mkdtemp 自建自删,确定性可重跑):
|
|
708
702
|
// - session 文件名带时间戳前缀(<ISO 时间戳>_<sessionId>.jsonl,真实 pi 落盘形态)
|
|
709
703
|
// - 混合新旧机制:A 顶层(无 parentRecordId)→ B 嵌套(manifest.parentRecordId 精确链);
|
|
710
704
|
// C 旧机制顶层(manifest/identity 均无 parentRecordId → flat 挂 main)
|
|
@@ -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 {
|
|
6
|
+
import { FIX_E6 } from './fixtures.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* 建一个假 session 文件:首行 header(type=session,含 id/cwd/parentSession),
|
|
@@ -534,61 +534,21 @@ describe('findSessions', () => {
|
|
|
534
534
|
// B 不入选(uuid 命中短路,不走 task 匹配)
|
|
535
535
|
expect(matches.some((m) => m.sessionId === idB)).toBe(false)
|
|
536
536
|
})
|
|
537
|
-
|
|
538
|
-
it.skipIf(!HAS_REAL_SUBAGENTS_DIR)(
|
|
539
|
-
'TC-u5-real-data-guard:find codex source:subagent 命中(本机有 codex 相关 subagent task)',
|
|
540
|
-
async () => {
|
|
541
|
-
const { matches, truncated } = await findSessions('codex', REAL_AGENT_DIR, {
|
|
542
|
-
source: 'subagent',
|
|
543
|
-
limit: 50,
|
|
544
|
-
})
|
|
545
|
-
expect(matches.length).toBeGreaterThan(0)
|
|
546
|
-
expect(matches.every((m) => m.source === 'subagent')).toBe(true)
|
|
547
|
-
expect(typeof truncated).toBe('boolean')
|
|
548
|
-
},
|
|
549
|
-
REAL_DATA_TIMEOUT_MS,
|
|
550
|
-
)
|
|
551
537
|
})
|
|
552
538
|
|
|
553
|
-
it
|
|
554
|
-
|
|
539
|
+
it('uuid 片段命中候选补全元数据:fileName/mtime/sizeBytes 真实值(019e6c96 锚)', async () => {
|
|
540
|
+
// 2 条 session、恰 1 条 id 含片段 'e6c96':uuid 片段路径命中后仍补读元数据字段。
|
|
541
|
+
// 命中文件按真实 pi 落盘命名(<id>.jsonl),fileName 断言含该 id
|
|
542
|
+
await makeSession(slugDir, { name: `${FIX_E6}.jsonl`, id: FIX_E6, cwd: '/demo' })
|
|
543
|
+
await makeSession(slugDir, { name: 'other.jsonl', id: '019ffff-cccc-dddd', cwd: '/demo' })
|
|
544
|
+
|
|
545
|
+
const { matches } = await findSessions('e6c96', agentDir)
|
|
555
546
|
expect(matches.length).toBeGreaterThan(0)
|
|
556
|
-
expect(matches.some((m) => m.sessionId.startsWith('019e6c96'))).toBe(true)
|
|
557
|
-
// 真实值校验
|
|
558
547
|
const hit = matches.find((m) => m.sessionId.startsWith('019e6c96'))!
|
|
559
548
|
expect(hit.fileName).toContain('019e6c96')
|
|
560
549
|
expect(hit.mtime).toBeGreaterThan(0)
|
|
561
550
|
expect(hit.sizeBytes).toBeGreaterThan(0)
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
it.skipIf(!HAS_E6)("真实数据:recent 返回最近 N 个,mtime 倒序,truncated=true", async () => {
|
|
565
|
-
const { matches, truncated } = await findSessions('recent', REAL_AGENT_DIR, { limit: 5 })
|
|
566
|
-
expect(matches.length).toBeGreaterThan(0)
|
|
567
|
-
expect(matches.length).toBeLessThanOrEqual(5)
|
|
568
|
-
// mtime 倒序
|
|
569
|
-
for (let i = 1; i < matches.length; i++) {
|
|
570
|
-
expect(matches[i - 1].mtime).toBeGreaterThanOrEqual(matches[i].mtime)
|
|
571
|
-
}
|
|
572
|
-
// 真实 session 文件远多于 5 → 截断
|
|
573
|
-
expect(truncated).toBe(true)
|
|
574
|
-
}, REAL_DATA_TIMEOUT_MS)
|
|
575
|
-
|
|
576
|
-
it.skipIf(!HAS_REAL_SUBAGENTS_DIR)(
|
|
577
|
-
"真实数据:source:'subagent' 能找到 completed subagent(§7 场景 1 find 部分)",
|
|
578
|
-
async () => {
|
|
579
|
-
const { matches } = await findSessions('recent', REAL_AGENT_DIR, {
|
|
580
|
-
source: 'subagent',
|
|
581
|
-
limit: 5,
|
|
582
|
-
})
|
|
583
|
-
expect(matches.length).toBeGreaterThan(0)
|
|
584
|
-
// 全部 source==='subagent',fileName 在 subagents/ 目录下
|
|
585
|
-
for (const m of matches) {
|
|
586
|
-
expect(m.source).toBe('subagent')
|
|
587
|
-
expect(m.fileName).toContain('subagents')
|
|
588
|
-
}
|
|
589
|
-
},
|
|
590
|
-
REAL_DATA_TIMEOUT_MS,
|
|
591
|
-
)
|
|
551
|
+
})
|
|
592
552
|
})
|
|
593
553
|
|
|
594
554
|
// ============================================================
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 测试 fixture 常量(跨测试文件共享的 uuid 形态标识符)。
|
|
3
|
+
*
|
|
4
|
+
* 全部用例的项目内数据 = 各测试文件用 mkdtemp 自建自删的最小 session 文件
|
|
5
|
+
*(按测试断言逻辑构造,不读本机真实用户数据目录)。本文件只登记 uuid 常量,
|
|
6
|
+
* 取值约束:
|
|
7
|
+
* - 合法 uuid 形态(满足 extractSessionIdFromFilename / header id 谓词);
|
|
8
|
+
* - FIX_E6 含十六进制片段 'e6c96',作 uuid 片段匹配路径(find/resolveSessionId)的锚 id,
|
|
9
|
+
* 且完整 id 以 '019e6c96' 开头,供前缀断言。
|
|
10
|
+
*
|
|
11
|
+
* 本文件无 .test.ts 后缀,vitest include 只收集 `src/__tests__/` 下以 .test.ts 结尾的文件,不收集本文件。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 主 fixture session id:片段 'e6c96' 命中 + 完整 id '019e6c96' 前缀断言的锚。 */
|
|
15
|
+
export const FIX_E6 = '019e6c96-0a0c-74b8-a73f-d1854d88e2a7'
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
2
5
|
import { parseSessionContent, parseSessionFile, type Entry } from '@zhushanwen/session-core'
|
|
3
|
-
import { REAL_SESSION, HAS_REAL_SESSION } from './real-data.js'
|
|
4
6
|
|
|
5
7
|
/** 构造单行 JSONL entry 字符串 */
|
|
6
8
|
function line(obj: Record<string, unknown>): string {
|
|
@@ -157,14 +159,37 @@ describe('parseSessionContent', () => {
|
|
|
157
159
|
})
|
|
158
160
|
|
|
159
161
|
describe('parseSessionFile', () => {
|
|
160
|
-
it
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
it('磁盘文件全量解析:entries 数 = 行数 / 0 skipped / 非 partial / totalBytes 与内容一致', async () => {
|
|
163
|
+
// mkdtemp 自建自删的 fixture 文件(不读本机真实数据目录):session header +
|
|
164
|
+
// 10 条链式 message 的最小行集,验证「文件读取 → 逐行解析」回路的完整性与计数字段
|
|
165
|
+
const dir = await mkdtemp(join(tmpdir(), 'parser-file-'))
|
|
166
|
+
try {
|
|
167
|
+
const lines = [
|
|
168
|
+
line({ type: 'session', id: 's1', parentId: null, cwd: '/x' }),
|
|
169
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
170
|
+
line({
|
|
171
|
+
type: 'message',
|
|
172
|
+
id: `m${i}`,
|
|
173
|
+
parentId: i === 0 ? 's1' : `m${i - 1}`,
|
|
174
|
+
message: {
|
|
175
|
+
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
176
|
+
content: [{ type: 'text', text: `text ${i}` }],
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
]
|
|
181
|
+
const content = lines.join('\n') + '\n'
|
|
182
|
+
const path = join(dir, 'fixture-session.jsonl')
|
|
183
|
+
await writeFile(path, content)
|
|
184
|
+
|
|
185
|
+
const result = await parseSessionFile(path)
|
|
186
|
+
|
|
187
|
+
expect(result.entries).toHaveLength(11)
|
|
188
|
+
expect(result.skippedLines).toBe(0)
|
|
189
|
+
expect(result.lastLinePartial).toBe(false)
|
|
190
|
+
expect(result.totalBytes).toBe(Buffer.byteLength(content, 'utf8'))
|
|
191
|
+
} finally {
|
|
192
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
193
|
+
}
|
|
194
|
+
})
|
|
170
195
|
})
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import { renderOutline, renderExpand, renderDetail, type ToolResultSummaryEntry } from '../core/render.js'
|
|
3
|
-
import {
|
|
3
|
+
import type { Entry } from '@zhushanwen/session-core'
|
|
4
4
|
import type { Turn } from '../core/turns.js'
|
|
5
5
|
import type { TreeView } from '../core/tree.js'
|
|
6
|
-
import { buildTreeView } from '../core/tree.js'
|
|
7
|
-
import { segmentTurns } from '../core/turns.js'
|
|
8
|
-
import { REAL_SESSION, HAS_REAL_SESSION } from './real-data.js'
|
|
9
6
|
|
|
10
7
|
// ---- 构造助手 ----
|
|
11
8
|
|
|
@@ -203,25 +200,32 @@ describe('renderOutline', () => {
|
|
|
203
200
|
expect(noBranch.lines).toEqual(['T000 · hi'])
|
|
204
201
|
})
|
|
205
202
|
|
|
206
|
-
it
|
|
207
|
-
// v2 O1:加 assistantBrief + 修 toolSummary bug 后 outline
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
203
|
+
it('8. 多 turn outline:tokenEstimate <= 1500 + assistantBrief/toolSummary 非空 + stats 完整(v2 O1)', () => {
|
|
204
|
+
// v2 O1:加 assistantBrief + 修 toolSummary bug 后 outline 行含结论与工具聚合,
|
|
205
|
+
// 阈值 1500(design §3.3 D4)。3 turn 合成数据,每 turn 带 toolCall + toolResult
|
|
206
|
+
const turns = [0, 1, 2].map((i) =>
|
|
207
|
+
turn(
|
|
208
|
+
i,
|
|
209
|
+
[
|
|
210
|
+
uEntry(`U${i}`, `问题 ${i}`),
|
|
211
|
+
aEntry(`A${i}`, `结论 ${i}`, [{ name: 'bash' }, { name: 'read' }]),
|
|
212
|
+
tEntry(`T${i}`, 'output'),
|
|
213
|
+
],
|
|
214
|
+
{ userEntry: uEntry(`U${i}`, `问题 ${i}`) },
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
const result = renderOutline(turns, emptyTree(), { budget: 2000 })
|
|
212
218
|
|
|
213
|
-
expect(result.turns.length).toBe(
|
|
219
|
+
expect(result.turns.length).toBe(3)
|
|
214
220
|
expect(result.tokenEstimate).toBeLessThanOrEqual(1500)
|
|
215
|
-
// v2 O1 验证:assistant 结论行存在(非空)+ toolSummary
|
|
221
|
+
// v2 O1 验证:assistant 结论行存在(非空)+ toolSummary 显示工具(修 v1 恒空 bug)
|
|
216
222
|
expect(result.turns.some((b) => b.assistantBrief !== '')).toBe(true)
|
|
217
223
|
expect(result.turns.some((b) => b.toolSummary !== '')).toBe(true)
|
|
218
224
|
// stats 完整性
|
|
219
|
-
expect(result.stats.totalTurns).toBe(
|
|
220
|
-
// totalEntries 近似(leaf+branch+orphan)不含 session header(segmentTurns 规则1
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// 5.6MB 全量解析在并发/高负载下可能超 vitest 默认 5s,显式放宽
|
|
224
|
-
}, 60000)
|
|
225
|
+
expect(result.stats.totalTurns).toBe(3)
|
|
226
|
+
// totalEntries 近似(leaf+branch+orphan)不含 session header(segmentTurns 规则1 跳过)
|
|
227
|
+
expect(result.stats.totalEntries).toBeGreaterThan(0)
|
|
228
|
+
})
|
|
225
229
|
})
|
|
226
230
|
|
|
227
231
|
describe('renderExpand', () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
|
-
import { mkdtemp, mkdir,
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
4
4
|
import { join, dirname } from 'node:path'
|
|
5
5
|
import {
|
|
6
6
|
resolveSessionRoots,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type SessionRootSignals,
|
|
10
10
|
} from '../discovery/roots.js'
|
|
11
11
|
import { buildFamilyFromFs } from '../discovery/subagents.js'
|
|
12
|
-
import {
|
|
12
|
+
import { FIX_E6 } from './fixtures.js'
|
|
13
13
|
|
|
14
14
|
// ============================================================
|
|
15
15
|
// A3(ext-simplify-04 U4):listMainSessions/listSubagentSessions 薄包装已删除,
|
|
@@ -35,7 +35,7 @@ async function writeJsonl(path: string, content = '{"type":"session","id":"x"}\n
|
|
|
35
35
|
await writeFile(path, content)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** 真实形态的 cwd 编码目录名(encodeCwd:`--` 开头 `--`
|
|
38
|
+
/** 真实形态的 cwd 编码目录名(encodeCwd:`--` 开头 `--` 结尾) */
|
|
39
39
|
const SLUG = '--Users-foo--'
|
|
40
40
|
|
|
41
41
|
describe('main 根过滤(原 listMainSessions 等价式,A3 退役后保覆盖)', () => {
|
|
@@ -100,8 +100,18 @@ describe('main 根过滤(原 listMainSessions 等价式,A3 退役后保覆
|
|
|
100
100
|
await expect(mainSessionFiles(join(dir, 'no-such-dir'))).resolves.toEqual([])
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
it
|
|
104
|
-
|
|
103
|
+
it('扫描 sessions/:含目标 session,排除 .finalized 与 workflow-state 目录(019e6c96 锚)', async () => {
|
|
104
|
+
// wf-state 文件(wf-*.jsonl)经 workflow-state 目录跳过排除——main 根无独立的
|
|
105
|
+
// wf- 文件名前缀过滤,slug 顶层放 wf-*.jsonl 会被收进候选集(该形态不在真实
|
|
106
|
+
// 落盘布局中出现:wf-state 恒在 workflow-state/ 目录内)
|
|
107
|
+
const slug = '--Users-foo--'
|
|
108
|
+
const slugDir = join(dir, 'sessions', slug)
|
|
109
|
+
await mkdir(join(slugDir, 'workflow-state'), { recursive: true })
|
|
110
|
+
await writeFile(join(slugDir, `${FIX_E6}.jsonl`), `{"type":"session","id":"${FIX_E6}"}\n`)
|
|
111
|
+
await writeFile(join(slugDir, 'b.jsonl.finalized'), '{"type":"session","id":"b"}\n')
|
|
112
|
+
await writeFile(join(slugDir, 'workflow-state', 'wf-inner.jsonl'), '{"v":"wf-run-v1"}\n')
|
|
113
|
+
|
|
114
|
+
const result = await mainSessionFiles(dir)
|
|
105
115
|
expect(result.length).toBeGreaterThan(0)
|
|
106
116
|
// 含目标 session
|
|
107
117
|
expect(result.some((m) => m.path.includes('019e6c96'))).toBe(true)
|
|
@@ -109,11 +119,9 @@ describe('main 根过滤(原 listMainSessions 等价式,A3 退役后保覆
|
|
|
109
119
|
expect(result.every((m) => !m.path.endsWith('.finalized'))).toBe(true)
|
|
110
120
|
// 排除 workflow-state 目录
|
|
111
121
|
expect(result.every((m) => !m.path.includes('workflow-state'))).toBe(true)
|
|
112
|
-
// 排除 wf- 前缀文件名
|
|
113
|
-
expect(result.every((m) => !m.path.split('/').pop()!.startsWith('wf-'))).toBe(true)
|
|
114
122
|
// mtime/size 真实
|
|
115
123
|
expect(result.every((m) => m.mtime > 0 && m.size > 0)).toBe(true)
|
|
116
|
-
}
|
|
124
|
+
})
|
|
117
125
|
})
|
|
118
126
|
|
|
119
127
|
describe('subagent 根选择(原 listSubagentSessions 等价式,A3 退役后保覆盖)', () => {
|
|
@@ -156,12 +164,6 @@ describe('subagent 根选择(原 listSubagentSessions 等价式,A3 退役后
|
|
|
156
164
|
it('无 subagents 目录返回 [],不抛错', async () => {
|
|
157
165
|
await expect(subagentSessionFiles(dir)).resolves.toEqual([])
|
|
158
166
|
})
|
|
159
|
-
|
|
160
|
-
it.skipIf(!HAS_REAL_SUBAGENTS_DIR)('真实数据:扫描 ~/.pi/agent/subagents 返回非空', async () => {
|
|
161
|
-
const result = await subagentSessionFiles(REAL_AGENT_DIR)
|
|
162
|
-
expect(result.length).toBeGreaterThan(0)
|
|
163
|
-
expect(result.every((m) => !m.path.endsWith('.finalized'))).toBe(true)
|
|
164
|
-
}, REAL_DATA_TIMEOUT_MS)
|
|
165
167
|
})
|
|
166
168
|
|
|
167
169
|
// ============================================================
|
|
@@ -320,26 +322,22 @@ describe('resolveSessionRoots', () => {
|
|
|
320
322
|
await expect(resolveSessionRoots({ agentDir: '' })).resolves.toEqual([])
|
|
321
323
|
})
|
|
322
324
|
|
|
323
|
-
it
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
expect(encodeCwdCount).toBeGreaterThan(0)
|
|
340
|
-
},
|
|
341
|
-
30000,
|
|
342
|
-
)
|
|
325
|
+
it('encodeCwd 判据自洽:--xx-- 形态子目录剥层到 sessions 根,普通名子目录取自身', async () => {
|
|
326
|
+
// 两种目录形态各造一个:encodeCwd 编码目录(剥层分支)与普通目录名(取自身分支)
|
|
327
|
+
const tmp = await mkdtemp(join(tmpdir(), 'roots-encode-'))
|
|
328
|
+
try {
|
|
329
|
+
const sessionsDir = join(tmp, 'agent', 'sessions')
|
|
330
|
+
const encoded = join(sessionsDir, '--Users-foo--')
|
|
331
|
+
const plain = join(sessionsDir, 'permission-forwarding')
|
|
332
|
+
await writeJsonl(join(encoded, 'a.jsonl'))
|
|
333
|
+
await writeJsonl(join(plain, 'p.jsonl'))
|
|
334
|
+
// 判据自洽:匹配 encodeCwd 形态 → 剥层到父目录;不匹配(如 permission-forwarding/)→ 取自身
|
|
335
|
+
expect(normalizeLiveSessionDir(encoded)).toBe(sessionsDir)
|
|
336
|
+
expect(normalizeLiveSessionDir(plain)).toBe(plain)
|
|
337
|
+
} finally {
|
|
338
|
+
await rm(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
339
|
+
}
|
|
340
|
+
})
|
|
343
341
|
})
|
|
344
342
|
|
|
345
343
|
describe('buildFamilyFromFs not-found 文案(U1 并入:列实际扫描候选根)', () => {
|