@zhushanwen/pi-session-reader 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/__tests__/cross-package-subagent-core.test.ts +34 -25
- package/src/__tests__/entry-anchor.test.ts +3 -3
- package/src/__tests__/execution-tree.test.ts +85 -86
- package/src/__tests__/family-zcode.test.ts +24 -14
- package/src/__tests__/family.test.ts +4 -4
- package/src/__tests__/find.test.ts +50 -5
- package/src/__tests__/index.test.ts +3 -3
- package/src/__tests__/rebuild-manifest-dual-write.test.ts +29 -22
- package/src/__tests__/render.test.ts +2 -2
- package/src/__tests__/result.test.ts +20 -11
- package/src/__tests__/roots.test.ts +64 -26
- package/src/__tests__/subagents.test.ts +108 -52
- package/src/__tests__/tool-handler.test.ts +133 -83
- package/src/__tests__/workflow.test.ts +5 -5
- package/src/__tests__/zcode-manifest.test.ts +18 -10
- package/src/__tests__/zcode-routing.test.ts +21 -19
- package/src/__tests__/zcode-whitelist.test.ts +1 -1
- package/src/core/family.ts +6 -6
- package/src/core/render.ts +2 -2
- package/src/core/toolcall.ts +1 -1
- package/src/core/workflow.ts +9 -9
- package/src/discovery/entry-anchor.ts +5 -5
- package/src/discovery/find.ts +9 -1
- package/src/discovery/roots.ts +95 -7
- package/src/discovery/subagents.ts +24 -17
- package/src/discovery/whitelist.ts +4 -4
- package/src/discovery/workflows.ts +8 -8
- package/src/discovery/zcode-manifest.ts +22 -27
- package/src/extract.ts +1 -1
- package/src/index.ts +2 -2
- package/src/no-match.ts +1 -1
- package/src/result-action.ts +1 -1
- package/src/search-across.ts +3 -3
- package/src/tool-format.ts +12 -12
- package/src/tool-handler.ts +27 -19
- package/src/tui/session-command.ts +1 -1
- package/src/zcode-anchor-classify.ts +2 -2
|
@@ -228,7 +228,7 @@ describe('resolveFamily - 错误', () => {
|
|
|
228
228
|
// ============================================================
|
|
229
229
|
// U4: SubagentRef 富字段(manifest 主 / P-fallback identity 回退)
|
|
230
230
|
// 验证 buildFamilyIndex 从 identity entry.data 读 task/agent/model/status/sessionFile
|
|
231
|
-
// 填 SubagentRef,异名映射(data.agent → agentName
|
|
231
|
+
// 填 SubagentRef,异名映射(data.agent → agentName),检查放宽(只校验 rootSessionId+slug)。
|
|
232
232
|
// ============================================================
|
|
233
233
|
|
|
234
234
|
describe('U4: SubagentRef 富字段(buildFamilyIndex 透传)', () => {
|
|
@@ -276,7 +276,7 @@ describe('U4: SubagentRef 富字段(buildFamilyIndex 透传)', () => {
|
|
|
276
276
|
expect(sub?.status).toBeUndefined()
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
-
it('TC-u4-pfallback-no-identity: data 缺 rootSessionId →
|
|
279
|
+
it('TC-u4-pfallback-no-identity: data 缺 rootSessionId → 检查拒掉,不入 subagentsByRoot,不抛错', () => {
|
|
280
280
|
// 模拟无 identity(运行中/异常,尾行是 message 非 identity):data 只有 slug 无 rootSessionId
|
|
281
281
|
const noRoot: Entry = {
|
|
282
282
|
type: 'custom',
|
|
@@ -287,7 +287,7 @@ describe('U4: SubagentRef 富字段(buildFamilyIndex 透传)', () => {
|
|
|
287
287
|
}
|
|
288
288
|
const index = buildFamilyIndex([header(ROOT)], [noRoot], makeStats([ROOT]))
|
|
289
289
|
|
|
290
|
-
//
|
|
290
|
+
// 检查拒掉 → 不入 subagentsByRoot
|
|
291
291
|
expect(index.subagentsByRoot.size).toBe(0)
|
|
292
292
|
// resolveFamily 不抛错,subagents 空
|
|
293
293
|
const family = resolveFamily(ROOT, index)
|
|
@@ -331,7 +331,7 @@ describe('U4: SubagentRef 富字段(buildFamilyIndex 透传)', () => {
|
|
|
331
331
|
expect(sub?.sessionFile).toBeUndefined()
|
|
332
332
|
})
|
|
333
333
|
|
|
334
|
-
it('isSubagentIdentityData
|
|
334
|
+
it('isSubagentIdentityData 检查放宽:富字段部分缺失/全缺都通过(只校验 rootSessionId+slug)', () => {
|
|
335
335
|
const identities = [
|
|
336
336
|
subagentIdentityRich(SUB, ROOT, 's1', { task: 'only-task' }), // 部分富字段
|
|
337
337
|
subagentIdentity(SUB2, ROOT, 's2'), // 完全无富字段(旧 manifest 形态)
|
|
@@ -3,6 +3,10 @@ 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 {
|
|
7
|
+
resolveStoreSubagentSessionsDir,
|
|
8
|
+
resolveStoreSubagentRecordsDir,
|
|
9
|
+
} from '../discovery/roots.js'
|
|
6
10
|
import { FIX_E6 } from './fixtures.js'
|
|
7
11
|
|
|
8
12
|
/**
|
|
@@ -131,21 +135,31 @@ async function makeSubagentSession(
|
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
describe('findSessions', () => {
|
|
138
|
+
let root: string
|
|
134
139
|
let agentDir: string
|
|
135
140
|
let slugDir: string
|
|
136
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* subagent fixture 目录([session-store-layout-inversion] 新布局存储树
|
|
143
|
+
* `<root>/<rootSessionId>/subagents/sessions/`,resolveSessionRoots [subagent] 根
|
|
144
|
+
* 定向扫描路径;分区键取 FIX_SID,生产同源推导函数落盘)。
|
|
145
|
+
*/
|
|
137
146
|
let saDir: string
|
|
138
147
|
|
|
139
148
|
let recordsDir: string
|
|
140
149
|
|
|
150
|
+
/** 存储树分区 sid fixture(合法 pi sid 形态)。 */
|
|
151
|
+
const FIX_SID = '0aaaaaaa-bbbb-7ccc-dddd-0000000000f1'
|
|
152
|
+
|
|
141
153
|
beforeEach(async () => {
|
|
142
|
-
agentDir =
|
|
154
|
+
// 生产拓扑复刻(布局 B):agentDir = <dataDir>/agent
|
|
155
|
+
root = await mkdtemp(join(tmpdir(), 'find-test-'))
|
|
156
|
+
agentDir = join(root, 'agent')
|
|
143
157
|
slugDir = join(agentDir, 'sessions', '--Users-demo--')
|
|
144
|
-
saDir =
|
|
145
|
-
recordsDir =
|
|
158
|
+
saDir = resolveStoreSubagentSessionsDir(agentDir, FIX_SID)
|
|
159
|
+
recordsDir = resolveStoreSubagentRecordsDir(agentDir, FIX_SID)
|
|
146
160
|
})
|
|
147
161
|
afterEach(async () => {
|
|
148
|
-
await rm(
|
|
162
|
+
await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
149
163
|
})
|
|
150
164
|
|
|
151
165
|
it('uuid 片段匹配正确的 session(sessionId 含 query)', async () => {
|
|
@@ -359,6 +373,37 @@ describe('findSessions', () => {
|
|
|
359
373
|
expect(matches[0].fileName).toContain('subagents')
|
|
360
374
|
})
|
|
361
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
|
+
|
|
362
407
|
it("recent 也尊重 source 过滤(含不传 source 时两侧按 mtime 混合倒序)", async () => {
|
|
363
408
|
const mainPath = await makeSession(slugDir, { name: 'm.jsonl', id: 'r-main', cwd: '/demo' })
|
|
364
409
|
const subPath = await makeSession(saDir, { name: 's.jsonl', id: 'r-sub', cwd: '/demo' })
|
|
@@ -11,9 +11,9 @@ import { Check } from 'typebox/value'
|
|
|
11
11
|
* mock pi/ctx(as unknown as ExtensionAPI),测:
|
|
12
12
|
* 1. execute 错误路径 throw(W4:pi 只对 execute throw 置 isError:true——agent-loop
|
|
13
13
|
* 丢弃返回值里的 isError 字段,「错误轮被标成功」契约守护)
|
|
14
|
-
* 2. session_start handler 的 ctx.mode!=='tui'
|
|
14
|
+
* 2. session_start handler 的 ctx.mode!=='tui' 检查
|
|
15
15
|
* 3. registeredPis WeakSet 去重:同 pi 二次 session_start 不重复注册;不同 pi 可注册
|
|
16
|
-
* 4. typeof ctx.ui.addAutocompleteProvider
|
|
16
|
+
* 4. typeof ctx.ui.addAutocompleteProvider 运行时检查(ui 缺方法 → 跳过,不崩)
|
|
17
17
|
* 5. registerCommand + addAutocompleteProvider 组装(tui 模式下各注册一次)
|
|
18
18
|
* 6. TypeBox schema 与 SessionReadParams 对齐:合法 params 过、非法 action/scope 拒
|
|
19
19
|
* 7. U3 信号包采集降级:sessionManager 缺 getSessionDir 方法 / getSessionDir 抛错 →
|
|
@@ -271,7 +271,7 @@ describe('sessionReaderExtension - session_start TUI 注册', () => {
|
|
|
271
271
|
expect(fake2.registerCommand).toHaveBeenCalledTimes(1)
|
|
272
272
|
})
|
|
273
273
|
|
|
274
|
-
it('ctx.ui 无 addAutocompleteProvider(typeof
|
|
274
|
+
it('ctx.ui 无 addAutocompleteProvider(typeof 检查)→ 跳过注册不崩', () => {
|
|
275
275
|
const fake = makeFakePi()
|
|
276
276
|
sessionReaderExtension(fake.pi as unknown as ExtensionAPI)
|
|
277
277
|
const { ctx } = makeFakeCtx({ withUi: false })
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// 跨包集成([U4c / G4-S5③] record 持久化收敛 P4c 验收锚点):
|
|
2
|
-
// subagent-core
|
|
2
|
+
// subagent-core 重建/终态写侧产出的 manifest → session-reader identity 派生视图不降级。
|
|
3
3
|
//
|
|
4
4
|
// 断言链(设计 subagent-record-persistence-consolidation D5 前向兼容判定 + S5③):
|
|
5
5
|
// 1. manifest 被人为删除(S5 缓存可丢场景)→ subagent-core RecordStore.rebuildIndexes
|
|
6
6
|
// 全量重建 → session-reader listRecordManifests 重新可见;
|
|
7
|
-
// 2. 重建窗口前后,session-reader identity
|
|
7
|
+
// 2. 重建窗口前后,session-reader identity 派生视图的输入字段集(rootSessionId/slug/
|
|
8
8
|
// task/agentName/model/status/sessionFile/parentRecordId)逐字段一致——双写过渡
|
|
9
|
-
// 字段(executionStatus)在场且不被本包消费(旧 status
|
|
9
|
+
// 字段(executionStatus)在场且不被本包消费(旧 status 三态派生视图继续生效);
|
|
10
10
|
// 3. 重建源 = 事件流折叠 + identity entry([③ .state 退场] 后终态位消失)——
|
|
11
11
|
// 身份/富字段逐字段保真;legacy status 不再保真 closed(重建单规则恒 idle +
|
|
12
|
-
// 无 closedReason → derivedManifestRecord
|
|
12
|
+
// 无 closedReason → derivedManifestRecord 如实派生 legacy "running",终态语义
|
|
13
13
|
// 由 record-settled 事件帧承载)。
|
|
14
14
|
//
|
|
15
15
|
// 形态同 cross-package-subagent-core.test.ts:subagent-core 真实 tmpdir 产出磁盘
|
|
@@ -32,7 +32,7 @@ import { listRecordManifests, type RecordManifest } from '../discovery/subagents
|
|
|
32
32
|
|
|
33
33
|
const ROOT_SESSION = 'root-dual-write'
|
|
34
34
|
|
|
35
|
-
/** session-reader identity
|
|
35
|
+
/** session-reader identity 派生视图(manifestIdentityData)消费的字段集——重建窗口
|
|
36
36
|
* 前后必须逐字段一致(降级 = 任一字段缺失/漂移)。 */
|
|
37
37
|
function identityView(m: RecordManifest) {
|
|
38
38
|
return {
|
|
@@ -49,15 +49,21 @@ function identityView(m: RecordManifest) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
describe('跨包集成:subagent-core rebuildIndexes 重建 manifest → session-reader 投影不降级', () => {
|
|
52
|
+
let dataDir: string
|
|
52
53
|
let agentDir: string
|
|
53
54
|
let sessionsDir: string
|
|
54
55
|
let recordsDir: string
|
|
55
56
|
let childFile: string
|
|
56
57
|
|
|
57
58
|
beforeEach(() => {
|
|
58
|
-
agentDir =
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// 生产拓扑复刻([session-store-layout-inversion 域 9]):agentDir = <dataDir>/agent
|
|
60
|
+
//(session-reader 的 agentDir 语义),subagent-core 写侧经新公式落存储树
|
|
61
|
+
// <dataDir>/ext-sessions/<rootSid>/subagents/——listRecordManifests 双根扫描的新根
|
|
62
|
+
//(dirname(agentDir)/ext-sessions)与写侧落点相遇
|
|
63
|
+
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sr-rebuild-dualwrite-'))
|
|
64
|
+
agentDir = path.join(dataDir, 'agent')
|
|
65
|
+
sessionsDir = getSubagentSessionDir(ROOT_SESSION, dataDir)
|
|
66
|
+
recordsDir = getSubagentRecordsDir(ROOT_SESSION, dataDir)
|
|
61
67
|
fs.mkdirSync(sessionsDir, { recursive: true })
|
|
62
68
|
fs.mkdirSync(recordsDir, { recursive: true })
|
|
63
69
|
|
|
@@ -95,11 +101,11 @@ describe('跨包集成:subagent-core rebuildIndexes 重建 manifest → sessio
|
|
|
95
101
|
})
|
|
96
102
|
|
|
97
103
|
afterEach(() => {
|
|
98
|
-
fs.rmSync(
|
|
104
|
+
fs.rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
99
105
|
})
|
|
100
106
|
|
|
101
|
-
it('S5③: manifest 删除 → rebuildIndexes 重建 → identity
|
|
102
|
-
// ── 初始 manifest:subagent-core
|
|
107
|
+
it('S5③: manifest 删除 → rebuildIndexes 重建 → identity 派生视图字段集前后一致(双写字段在场)', async () => {
|
|
108
|
+
// ── 初始 manifest:subagent-core 终态写侧(markFinalized)真实产出 ──
|
|
103
109
|
const record = createRecord('sa-dw', {
|
|
104
110
|
agent: 'worker',
|
|
105
111
|
model: 'prov/m1',
|
|
@@ -119,7 +125,8 @@ describe('跨包集成:subagent-core rebuildIndexes 重建 manifest → sessio
|
|
|
119
125
|
closedReason: 'gc' as const,
|
|
120
126
|
endedAt: 2000,
|
|
121
127
|
})
|
|
122
|
-
|
|
128
|
+
// [u-subagent dir 晚绑定] RecordStore 构造 = 目录取值函数(非目录字符串)
|
|
129
|
+
const writer = new RecordStore(() => sessionsDir, undefined, undefined, () => recordsDir)
|
|
123
130
|
expect(writer.markFinalized(record, 'gc')).toBe(true)
|
|
124
131
|
writer.dispose()
|
|
125
132
|
|
|
@@ -127,9 +134,9 @@ describe('跨包集成:subagent-core rebuildIndexes 重建 manifest → sessio
|
|
|
127
134
|
expect(before).toBeDefined()
|
|
128
135
|
const viewBefore = identityView(before!)
|
|
129
136
|
|
|
130
|
-
//
|
|
137
|
+
// 双写过渡字段在写侧就位(本包不消费,但前向兼容锚要求其在场)。
|
|
131
138
|
// [U2 两态] executionStatus 是内部权威词汇(running|idle 两态,终态概念删除)
|
|
132
|
-
// → 终态化后恒 'idle';status 旧三态是 session-reader
|
|
139
|
+
// → 终态化后恒 'idle';status 旧三态是 session-reader 直读派生视图(永久保留)→ 'closed'。
|
|
133
140
|
const rawBefore = JSON.parse(fs.readFileSync(path.join(recordsDir, 'sa-dw.json'), 'utf-8')) as Record<string, unknown>
|
|
134
141
|
expect(rawBefore.executionStatus).toBe('idle')
|
|
135
142
|
expect(rawBefore.status).toBe('closed')
|
|
@@ -138,15 +145,15 @@ describe('跨包集成:subagent-core rebuildIndexes 重建 manifest → sessio
|
|
|
138
145
|
for (const f of fs.readdirSync(recordsDir)) fs.rmSync(path.join(recordsDir, f))
|
|
139
146
|
expect((await listRecordManifests(agentDir)).filter((m) => m.id === 'sa-dw')).toHaveLength(0)
|
|
140
147
|
|
|
141
|
-
const rebuilder = new RecordStore(sessionsDir, undefined, undefined, recordsDir)
|
|
148
|
+
const rebuilder = new RecordStore(() => sessionsDir, undefined, undefined, () => recordsDir)
|
|
142
149
|
expect(rebuilder.rebuildIndexes()).toBe(1)
|
|
143
150
|
rebuilder.dispose()
|
|
144
151
|
|
|
145
|
-
// ── session-reader 视角:重建后 identity
|
|
152
|
+
// ── session-reader 视角:重建后 identity 派生视图不降级 ──
|
|
146
153
|
const after = (await listRecordManifests(agentDir)).find((m) => m.id === 'sa-dw')
|
|
147
154
|
expect(after).toBeDefined()
|
|
148
155
|
// 字段集保真(身份/富字段逐字段一致);仅 legacy status 按 ③ 后重建语义如实
|
|
149
|
-
//
|
|
156
|
+
// 派生 running(终态权威 = 事件流 record-settled 帧,磁盘重建无 closedReason 载体)。
|
|
150
157
|
expect(identityView(after!)).toEqual({ ...viewBefore, status: 'running' })
|
|
151
158
|
|
|
152
159
|
const rawAfter = JSON.parse(fs.readFileSync(path.join(recordsDir, 'sa-dw.json'), 'utf-8')) as Record<string, unknown>
|
|
@@ -156,10 +163,10 @@ describe('跨包集成:subagent-core rebuildIndexes 重建 manifest → sessio
|
|
|
156
163
|
expect(rawAfter.closedReason).toBeUndefined()
|
|
157
164
|
})
|
|
158
165
|
|
|
159
|
-
it('
|
|
160
|
-
//
|
|
166
|
+
it('惰性通道:查询结果(collectRecords)补建的 manifest 同样可被本包读取派生视图', async () => {
|
|
167
|
+
// 不经终态写侧(无 manifest 初始形态):磁盘 record 只有 identity + 无 sidecar
|
|
161
168
|
// → 查询触发惰性补建。
|
|
162
|
-
const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir)
|
|
169
|
+
const store = new RecordStore(() => sessionsDir, undefined, undefined, () => recordsDir)
|
|
163
170
|
expect(store.collectRecords(10).map((r) => r.id)).toContain('sa-dw')
|
|
164
171
|
store.dispose()
|
|
165
172
|
|
|
@@ -172,8 +179,8 @@ describe('跨包集成:subagent-core rebuildIndexes 重建 manifest → sessio
|
|
|
172
179
|
expect(found!.sessionFile).toBe(childFile)
|
|
173
180
|
expect(found!.parentRecordId).toBe('sa-parent')
|
|
174
181
|
// 双写过渡字段([U3 重建单规则] 无 sidecar 重建一律 idle:executionStatus 如实
|
|
175
|
-
//
|
|
176
|
-
//
|
|
182
|
+
// 派生两态词汇;legacy status 经桥接判据派生——无 closedReason 遗留位的 idle
|
|
183
|
+
// 派生 "running"(session-reader 视角的活跃成员,§3.2.8 下行映射)。
|
|
177
184
|
const raw = JSON.parse(fs.readFileSync(path.join(recordsDir, 'sa-dw.json'), 'utf-8')) as Record<string, unknown>
|
|
178
185
|
expect(raw.executionStatus).toBe('idle')
|
|
179
186
|
expect(raw.status).toBe('running')
|
|
@@ -254,7 +254,7 @@ describe('renderOutline', () => {
|
|
|
254
254
|
expect(result.stats.totalTurns).toBe(2)
|
|
255
255
|
// T 序号 = 原 turn.index(expand/detail 的入口地址稳定,折叠不重编号)
|
|
256
256
|
expect(result.turns.map((b) => b.index)).toEqual([1, 3])
|
|
257
|
-
//
|
|
257
|
+
// 字节/条目统计保持全量口径(折叠是展示层行为,不丢数据):2+1+1+1 = 5
|
|
258
258
|
expect(result.stats.totalEntries).toBe(5)
|
|
259
259
|
})
|
|
260
260
|
|
|
@@ -459,7 +459,7 @@ describe('renderDetail', () => {
|
|
|
459
459
|
})
|
|
460
460
|
})
|
|
461
461
|
|
|
462
|
-
//
|
|
462
|
+
// 测试内联的类型检查镜像(验证 renderDetail 剥离效果,不依赖 render 内部导出)
|
|
463
463
|
function extractTextForTest(content: unknown): string {
|
|
464
464
|
if (typeof content === 'string') return content
|
|
465
465
|
if (Array.isArray(content)) {
|
|
@@ -4,6 +4,10 @@ import { tmpdir } from 'node:os'
|
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { handleSessionRead, type SessionReadParams } from '../tool-handler.js'
|
|
6
6
|
import { extractFinalAssistantText } from '../result-action.js'
|
|
7
|
+
import {
|
|
8
|
+
resolveStoreSubagentSessionsDir,
|
|
9
|
+
resolveStoreSubagentRecordsDir,
|
|
10
|
+
} from '../discovery/roots.js'
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* U6 result action 测试(design subagent-sync-collect §3.1.3 + impl-plan U6)。
|
|
@@ -22,9 +26,11 @@ import { extractFinalAssistantText } from '../result-action.js'
|
|
|
22
26
|
* 一致的前置,A4 取回门)。
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
|
-
// ---- fixture 工具(合成 subagent session,同 tool-handler.test.ts makeFixtureSubagent
|
|
29
|
+
// ---- fixture 工具(合成 subagent session,同 tool-handler.test.ts makeFixtureSubagent 结构;
|
|
30
|
+
// [session-store-layout-inversion] 新布局存储树,分区键 = manifest.rootSessionId 常量)----
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
/** 存储树分区 sid fixture(合法 pi sid 形态;与 makeSubagent 内 manifest.rootSessionId 同值)。 */
|
|
33
|
+
const FIX_ROOT_SID = '0aaaaaaa-bbbb-7ccc-dddd-0000000000r1'
|
|
28
34
|
|
|
29
35
|
interface SubagentFixture {
|
|
30
36
|
saId: string
|
|
@@ -45,9 +51,10 @@ function assistantEntry(id: string, content: unknown): unknown {
|
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
async function makeSubagent(dir: string, f: SubagentFixture): Promise<string> {
|
|
48
|
-
const
|
|
54
|
+
const sessionsDir = resolveStoreSubagentSessionsDir(dir, FIX_ROOT_SID)
|
|
55
|
+
const sessionFile = join(sessionsDir, `${f.realId}.jsonl`)
|
|
49
56
|
if (f.sessionFileExists !== false) {
|
|
50
|
-
await mkdir(
|
|
57
|
+
await mkdir(sessionsDir, { recursive: true })
|
|
51
58
|
const header = JSON.stringify({ type: 'session', id: f.realId, cwd: '/demo' })
|
|
52
59
|
const task = JSON.stringify({
|
|
53
60
|
type: 'message',
|
|
@@ -58,13 +65,13 @@ async function makeSubagent(dir: string, f: SubagentFixture): Promise<string> {
|
|
|
58
65
|
const lines = [header, task, ...(f.entries ?? []).map((e) => JSON.stringify(e))]
|
|
59
66
|
await writeFile(sessionFile, lines.join('\n') + '\n')
|
|
60
67
|
}
|
|
61
|
-
const recordsDir =
|
|
68
|
+
const recordsDir = resolveStoreSubagentRecordsDir(dir, FIX_ROOT_SID)
|
|
62
69
|
await mkdir(recordsDir, { recursive: true })
|
|
63
70
|
await writeFile(
|
|
64
71
|
join(recordsDir, `${f.saId}.json`),
|
|
65
72
|
JSON.stringify({
|
|
66
73
|
id: f.saId,
|
|
67
|
-
rootSessionId:
|
|
74
|
+
rootSessionId: FIX_ROOT_SID,
|
|
68
75
|
agentName: 'explorer',
|
|
69
76
|
sessionFile,
|
|
70
77
|
}),
|
|
@@ -120,13 +127,15 @@ describe('extractFinalAssistantText(同源语义白盒)', () => {
|
|
|
120
127
|
// ---- result action 黑盒(fixture:sa-id manifest 反查主路径) ----
|
|
121
128
|
|
|
122
129
|
describe('result action(U6)', () => {
|
|
123
|
-
let
|
|
130
|
+
let dataDir: string
|
|
131
|
+
let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
|
|
124
132
|
|
|
125
133
|
beforeEach(async () => {
|
|
126
|
-
|
|
134
|
+
dataDir = await mkdtemp(join(tmpdir(), 'session-reader-result-'))
|
|
135
|
+
dir = join(dataDir, 'agent')
|
|
127
136
|
})
|
|
128
137
|
afterEach(async () => {
|
|
129
|
-
await rm(
|
|
138
|
+
await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
130
139
|
})
|
|
131
140
|
|
|
132
141
|
it('1. 单 id:返回纯正文(无包装),details 带元数据(sessionId=header 真实 id)', async () => {
|
|
@@ -250,7 +259,7 @@ describe('result action(U6)', () => {
|
|
|
250
259
|
)
|
|
251
260
|
expect(cut.content[0]?.text).toBe(
|
|
252
261
|
'abcde\n\n[truncated 5 of 10 chars — full text: read ' +
|
|
253
|
-
`${join(dir,
|
|
262
|
+
`${join(resolveStoreSubagentSessionsDir(dir, FIX_ROOT_SID), `${realId}.jsonl`)}` +
|
|
254
263
|
', or session_read { action:"detail", session:"sa-lim" }]',
|
|
255
264
|
)
|
|
256
265
|
const exact = await handleSessionRead(
|
|
@@ -267,7 +276,7 @@ describe('result action(U6)', () => {
|
|
|
267
276
|
}
|
|
268
277
|
})
|
|
269
278
|
|
|
270
|
-
it('6. 不存在 id:sa-id 无 manifest → zcode_record_not_found(U9
|
|
279
|
+
it('6. 不存在 id:sa-id 无 manifest → zcode_record_not_found(U9 统一错误范围);uuid 片段零匹配 → 无匹配 session', async () => {
|
|
271
280
|
await expect(
|
|
272
281
|
handleSessionRead({ action: 'result', session: 'sa-nonexist-9999' }, { agentDir: dir }),
|
|
273
282
|
).rejects.toThrow(/zcode_record_not_found/)
|
|
@@ -4,6 +4,8 @@ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
|
4
4
|
import { join, dirname } from 'node:path'
|
|
5
5
|
import {
|
|
6
6
|
resolveSessionRoots,
|
|
7
|
+
resolveSessionStoreRoot,
|
|
8
|
+
resolveStoreSubagentSessionsDir,
|
|
7
9
|
normalizeLiveSessionDir,
|
|
8
10
|
type SessionRoot,
|
|
9
11
|
type SessionRootSignals,
|
|
@@ -38,6 +40,9 @@ async function writeJsonl(path: string, content = '{"type":"session","id":"x"}\n
|
|
|
38
40
|
/** 真实形态的 cwd 编码目录名(encodeCwd:`--` 开头 `--` 结尾) */
|
|
39
41
|
const SLUG = '--Users-foo--'
|
|
40
42
|
|
|
43
|
+
/** taiji 用例的存储树分区 sid fixture(合法 pi sid 形态)。 */
|
|
44
|
+
const TAIJI_SID = '0aaaaaaa-bbbb-7ccc-dddd-0000000000t1'
|
|
45
|
+
|
|
41
46
|
describe('main 根过滤(原 listMainSessions 等价式,A3 退役后保覆盖)', () => {
|
|
42
47
|
let dir: string
|
|
43
48
|
|
|
@@ -124,24 +129,26 @@ describe('main 根过滤(原 listMainSessions 等价式,A3 退役后保覆
|
|
|
124
129
|
})
|
|
125
130
|
})
|
|
126
131
|
|
|
127
|
-
describe('subagent 根选择(原 listSubagentSessions 等价式,A3
|
|
128
|
-
let
|
|
132
|
+
describe('subagent 根选择(原 listSubagentSessions 等价式,A3 退役后保覆盖;[session-store-layout-inversion] 单新根)', () => {
|
|
133
|
+
let dataDir: string
|
|
134
|
+
let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
|
|
129
135
|
|
|
130
136
|
beforeEach(async () => {
|
|
131
|
-
|
|
137
|
+
dataDir = await mkdtemp(join(tmpdir(), 'roots-sub-test-'))
|
|
138
|
+
dir = join(dataDir, 'agent')
|
|
132
139
|
})
|
|
133
140
|
afterEach(async () => {
|
|
134
|
-
await rm(
|
|
141
|
+
await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
135
142
|
})
|
|
136
143
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
144
|
+
/** 存储树分区 sid fixture(合法 pi sid 形态)。 */
|
|
145
|
+
const SID_P = '0aaaaaaa-bbbb-7ccc-dddd-0000000000p1'
|
|
146
|
+
|
|
147
|
+
it('扫描存储树 <root>/<sid>/subagents/sessions/*.jsonl,排除 .finalized', async () => {
|
|
148
|
+
const sessionsDir = resolveStoreSubagentSessionsDir(dir, SID_P)
|
|
149
|
+
await mkdir(sessionsDir, { recursive: true })
|
|
150
|
+
await writeFile(join(sessionsDir, 'c.jsonl'), '{"type":"session"}\n')
|
|
151
|
+
await writeFile(join(sessionsDir, 'c.jsonl.finalized'), '{"type":"session"}\n')
|
|
145
152
|
|
|
146
153
|
const result = await subagentSessionFiles(dir)
|
|
147
154
|
expect(result).toHaveLength(1)
|
|
@@ -149,21 +156,47 @@ describe('subagent 根选择(原 listSubagentSessions 等价式,A3 退役后
|
|
|
149
156
|
expect(result[0].path.endsWith('.finalized')).toBe(false)
|
|
150
157
|
})
|
|
151
158
|
|
|
152
|
-
it('records/ 子目录(.json manifest
|
|
153
|
-
const
|
|
154
|
-
await mkdir(
|
|
155
|
-
|
|
156
|
-
await
|
|
157
|
-
await writeFile(join(dir,
|
|
159
|
+
it('records/ 子目录(.json manifest)不被误收(同分区固定位置只探 sessions/)', async () => {
|
|
160
|
+
const { resolveStoreSubagentRecordsDir } = await import('../discovery/roots.js')
|
|
161
|
+
await mkdir(resolveStoreSubagentRecordsDir(dir, SID_P), { recursive: true })
|
|
162
|
+
const sessionsDir = resolveStoreSubagentSessionsDir(dir, SID_P)
|
|
163
|
+
await mkdir(sessionsDir, { recursive: true })
|
|
164
|
+
await writeFile(join(resolveStoreSubagentRecordsDir(dir, SID_P), 'manifest.json'), '{}\n')
|
|
165
|
+
await writeFile(join(sessionsDir, 'sub.jsonl'), '{"type":"session"}\n')
|
|
158
166
|
|
|
159
167
|
const result = await subagentSessionFiles(dir)
|
|
160
168
|
expect(result).toHaveLength(1)
|
|
161
169
|
expect(result[0].path.endsWith('sub.jsonl')).toBe(true)
|
|
162
170
|
})
|
|
163
171
|
|
|
164
|
-
it('
|
|
172
|
+
it('非会话分区目录(isPiSessionId 不过)剪枝跳过,不进 subagent 根候选', async () => {
|
|
173
|
+
// 非法分区名(前导 - 违反首字符规则;btw 虚拟 id 冒号形态同拒)
|
|
174
|
+
for (const bogus of ['-bogus', 'btw:0aaa']) {
|
|
175
|
+
const bogusDir = join(resolveSessionStoreRoot(dir), bogus, 'subagents', 'sessions')
|
|
176
|
+
await mkdir(bogusDir, { recursive: true })
|
|
177
|
+
await writeFile(join(bogusDir, 'x.jsonl'), '{"type":"session"}\n')
|
|
178
|
+
}
|
|
179
|
+
const okDir = resolveStoreSubagentSessionsDir(dir, SID_P)
|
|
180
|
+
await mkdir(okDir, { recursive: true })
|
|
181
|
+
await writeFile(join(okDir, 'ok.jsonl'), '{"type":"session"}\n')
|
|
182
|
+
|
|
183
|
+
const result = await subagentSessionFiles(dir)
|
|
184
|
+
expect(result).toHaveLength(1)
|
|
185
|
+
expect(result[0]!.path.endsWith('ok.jsonl')).toBe(true)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('无存储树目录返回 [],不抛错', async () => {
|
|
165
189
|
await expect(subagentSessionFiles(dir)).resolves.toEqual([])
|
|
166
190
|
})
|
|
191
|
+
|
|
192
|
+
it('D6 单新根锚定:旧布局 <agentDir>/subagents 造会话文件不被扫描(存量不可见)', async () => {
|
|
193
|
+
// 旧布局存量原位保留(D6 裁决:不迁移、不再读写)
|
|
194
|
+
const legacyDir = join(dir, 'subagents', '--legacy-cwd--', 'sessions')
|
|
195
|
+
await mkdir(legacyDir, { recursive: true })
|
|
196
|
+
await writeFile(join(legacyDir, 'legacy.jsonl'), '{"type":"session"}\n')
|
|
197
|
+
|
|
198
|
+
expect(await subagentSessionFiles(dir)).toEqual([])
|
|
199
|
+
})
|
|
167
200
|
})
|
|
168
201
|
|
|
169
202
|
// ============================================================
|
|
@@ -222,7 +255,8 @@ const HOST_CASES: HostCase[] = [
|
|
|
222
255
|
const agentDir = join(dataDir, 'agent')
|
|
223
256
|
await writeJsonl(join(agentDir, 'sessions', SLUG, 'x.jsonl'))
|
|
224
257
|
await writeJsonl(join(dataDir, 'sessions', 'old.jsonl')) // 旧旧布局迁移残留(§6.13 U1 行)
|
|
225
|
-
|
|
258
|
+
// [session-store-layout-inversion] subagent 会话在新布局存储树(生产同源推导落盘)
|
|
259
|
+
await writeJsonl(join(resolveStoreSubagentSessionsDir(agentDir, TAIJI_SID), 's.jsonl'))
|
|
226
260
|
return { agentDir, signals: { agentDir, liveSessionDir: join(agentDir, 'sessions') } }
|
|
227
261
|
},
|
|
228
262
|
verify({ agentDir, tmp }, roots) {
|
|
@@ -273,11 +307,12 @@ describe('resolveSessionRoots', () => {
|
|
|
273
307
|
const { agentDir, signals } = await build(tmp)
|
|
274
308
|
const roots = await resolveSessionRoots(signals)
|
|
275
309
|
|
|
276
|
-
// ④ subagent 根恒在(常量推导,不来自任何信号,§6.1 信号 5
|
|
310
|
+
// ④ subagent 根恒在(常量推导,不来自任何信号,§6.1 信号 5);
|
|
311
|
+
// [session-store-layout-inversion] 根 = session 存储树(dirname(agentDir)/ext-sessions)
|
|
277
312
|
const sub = roots.find((r) => r.kind === 'subagent')
|
|
278
313
|
expect(sub).toBeDefined()
|
|
279
314
|
expect(sub!.source).toBe('subagent')
|
|
280
|
-
expect(sub!.path).toBe(
|
|
315
|
+
expect(sub!.path).toBe(resolveSessionStoreRoot(agentDir))
|
|
281
316
|
|
|
282
317
|
// ⑤ 已扫描根 fileCount/scanMs 恒填充且与 files 一致;被去重根不产生扫描统计
|
|
283
318
|
for (const r of roots) {
|
|
@@ -389,8 +424,11 @@ describe('resolveSessionRoots options(u8:subagents 模式)', () => {
|
|
|
389
424
|
const agentDir = join(tmp, 'agent')
|
|
390
425
|
await mkdir(join(agentDir, 'sessions', SLUG), { recursive: true })
|
|
391
426
|
await writeFile(join(agentDir, 'sessions', SLUG, 'a.jsonl'), '{"type":"session"}\n')
|
|
392
|
-
await mkdir(
|
|
393
|
-
await writeFile(
|
|
427
|
+
await mkdir(resolveStoreSubagentSessionsDir(agentDir, '0aaaaaaa-bbbb-7ccc-dddd-0000000000o1'), { recursive: true })
|
|
428
|
+
await writeFile(
|
|
429
|
+
join(resolveStoreSubagentSessionsDir(agentDir, '0aaaaaaa-bbbb-7ccc-dddd-0000000000o1'), 's.jsonl'),
|
|
430
|
+
'{"type":"session"}\n',
|
|
431
|
+
)
|
|
394
432
|
return { tmp, agentDir }
|
|
395
433
|
}
|
|
396
434
|
|
|
@@ -428,8 +466,8 @@ describe('resolveSessionRoots options(u8:subagents 模式)', () => {
|
|
|
428
466
|
})
|
|
429
467
|
|
|
430
468
|
it('重复调用每次实扫(无缓存):两次结果独立完整,files 恒非空实扫值', async () => {
|
|
431
|
-
// 原三个 cache 用例(set 回写 / 命中优先 / stat × cache
|
|
432
|
-
// (ext-simplify-04 U3
|
|
469
|
+
// 原三个 cache 用例(set 回写 / 命中优先 / stat × cache)随缓存注入点删除
|
|
470
|
+
// (ext-simplify-04 U3);本用例固定删除后契约——不存在任何缓存层
|
|
433
471
|
const { tmp, agentDir } = await buildMinimalFixture()
|
|
434
472
|
try {
|
|
435
473
|
const first = await resolveSessionRoots({ agentDir })
|