@zhushanwen/pi-session-reader 0.6.0 → 0.7.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/README.md +100 -0
- package/package.json +7 -4
- package/src/__tests__/cross-package-subagent-core.test.ts +36 -44
- package/src/__tests__/entry-anchor.test.ts +226 -0
- package/src/__tests__/env.test.ts +54 -54
- package/src/__tests__/execution-tree.test.ts +6 -0
- package/src/__tests__/family-zcode.test.ts +255 -0
- package/src/__tests__/family.test.ts +1 -1
- package/src/__tests__/find.test.ts +24 -1
- package/src/__tests__/index.test.ts +1 -1
- package/src/__tests__/parser.test.ts +1 -2
- package/src/__tests__/real-data.ts +22 -9
- package/src/__tests__/render.test.ts +1 -2
- package/src/__tests__/result.test.ts +2 -2
- package/src/__tests__/roots.test.ts +1 -1
- package/src/__tests__/subagents.test.ts +1 -4
- package/src/__tests__/tool-handler.test.ts +76 -85
- package/src/__tests__/toolcall.test.ts +1 -1
- package/src/__tests__/tree.test.ts +1 -1
- package/src/__tests__/turns.test.ts +1 -2
- package/src/__tests__/zcode-manifest.test.ts +361 -0
- package/src/__tests__/zcode-routing.test.ts +751 -0
- package/src/__tests__/zcode-whitelist.test.ts +119 -0
- package/src/core/execution-tree.ts +3 -1
- package/src/core/family.ts +80 -1
- package/src/core/render.ts +2 -3
- package/src/core/toolcall.ts +3 -4
- package/src/core/tree.ts +1 -1
- package/src/core/turns.ts +1 -1
- package/src/core/units.ts +14 -0
- package/src/discovery/entry-anchor.ts +108 -0
- package/src/discovery/env.ts +41 -41
- package/src/discovery/find.ts +1 -1
- package/src/discovery/roots.ts +2 -2
- package/src/discovery/session-header.ts +30 -69
- package/src/discovery/subagents.ts +9 -2
- package/src/discovery/whitelist.ts +87 -0
- package/src/discovery/workflows.ts +1 -1
- package/src/discovery/zcode-manifest.ts +277 -0
- package/src/doctor.ts +5 -70
- package/src/index.ts +9 -2
- package/src/result-action.ts +15 -6
- package/src/search-across.ts +2 -5
- package/src/tool-format.ts +455 -0
- package/src/tool-handler.ts +321 -325
- package/src/zcode-anchor-classify.ts +60 -0
- package/vitest.config.ts +2 -4
- package/src/core/parser.ts +0 -167
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
detectEnvironment,
|
|
4
|
-
|
|
4
|
+
TAIJI_AGENT_EXT_LOG_ENV,
|
|
5
5
|
PI_CODING_AGENT_DIR_ENV,
|
|
6
|
-
|
|
6
|
+
TAIJI_AGENT_DATA_DIR_ENV,
|
|
7
7
|
type EnvironmentSignals,
|
|
8
8
|
} from '../discovery/env.js'
|
|
9
9
|
|
|
@@ -22,54 +22,54 @@ function signals(overrides: Partial<EnvironmentSignals> = {}): EnvironmentSignal
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
describe('detectEnvironment 托管合取判定', () => {
|
|
25
|
-
it('双信号齐备 + B 后新布局 <dataDir>/agent →
|
|
25
|
+
it('双信号齐备 + B 后新布局 <dataDir>/agent → taiji(packaged)', () => {
|
|
26
26
|
const r = detectEnvironment(
|
|
27
27
|
signals({
|
|
28
28
|
env: {
|
|
29
|
-
[
|
|
30
|
-
[PI_CODING_AGENT_DIR_ENV]: '/Users/u/.
|
|
29
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '1',
|
|
30
|
+
[PI_CODING_AGENT_DIR_ENV]: '/Users/u/.taiji/agent',
|
|
31
31
|
},
|
|
32
32
|
bundleUrl:
|
|
33
33
|
'file:///Applications/TaiJi.app/Contents/Resources/extensions/@zhushanwen/pi-session-reader/index.mjs',
|
|
34
34
|
}),
|
|
35
35
|
)
|
|
36
|
-
expect(r.kind).toBe('
|
|
36
|
+
expect(r.kind).toBe('taiji')
|
|
37
37
|
expect(r.distribution).toBe('packaged')
|
|
38
|
-
expect(r.dataDir).toBe('/Users/u/.
|
|
38
|
+
expect(r.dataDir).toBe('/Users/u/.taiji')
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it('双信号齐备 + B 前旧布局 <dataDir>/pi/agent →
|
|
41
|
+
it('双信号齐备 + B 前旧布局 <dataDir>/pi/agent → taiji(迁移窗口期双形态兼容)', () => {
|
|
42
42
|
const r = detectEnvironment(
|
|
43
43
|
signals({
|
|
44
44
|
env: {
|
|
45
|
-
[
|
|
46
|
-
[PI_CODING_AGENT_DIR_ENV]: '/Users/u/.
|
|
45
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '1',
|
|
46
|
+
[PI_CODING_AGENT_DIR_ENV]: '/Users/u/.taiji-dev/pi/agent',
|
|
47
47
|
},
|
|
48
|
-
bundleUrl: 'file:///Users/u/Code/
|
|
48
|
+
bundleUrl: 'file:///Users/u/Code/taiji/extensions/universal/session-reader/src/index.ts',
|
|
49
49
|
}),
|
|
50
50
|
)
|
|
51
|
-
expect(r.kind).toBe('
|
|
51
|
+
expect(r.kind).toBe('taiji')
|
|
52
52
|
expect(r.distribution).toBe('dev')
|
|
53
|
-
expect(r.dataDir).toBe('/Users/u/.
|
|
53
|
+
expect(r.dataDir).toBe('/Users/u/.taiji-dev')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
it('透传污染反例 1:仅
|
|
56
|
+
it('透传污染反例 1:仅 TAIJI_AGENT_EXT_LOG=1(PI_CODING_AGENT_DIR 未设)→ standalone-pi,evidence 点名孤置信号', () => {
|
|
57
57
|
const r = detectEnvironment(
|
|
58
|
-
signals({ env: { [
|
|
58
|
+
signals({ env: { [TAIJI_AGENT_EXT_LOG_ENV]: '1' } }),
|
|
59
59
|
)
|
|
60
60
|
expect(r.kind).toBe('standalone-pi')
|
|
61
61
|
expect(r.dataDir).toBeUndefined()
|
|
62
62
|
const joined = r.evidence.join('\n')
|
|
63
|
-
expect(joined).toContain(`${
|
|
63
|
+
expect(joined).toContain(`${TAIJI_AGENT_EXT_LOG_ENV}='1' → 命中`)
|
|
64
64
|
expect(joined).toContain('单独成立不合取')
|
|
65
|
-
expect(joined).toContain(
|
|
65
|
+
expect(joined).toContain(TAIJI_AGENT_EXT_LOG_ENV)
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
it('透传污染反例 2:仅
|
|
68
|
+
it('透传污染反例 2:仅 TAIJI_AGENT_EXT_LOG=1 且 PI_CODING_AGENT_DIR 为裸 pi 形态 → standalone-pi', () => {
|
|
69
69
|
const r = detectEnvironment(
|
|
70
70
|
signals({
|
|
71
71
|
env: {
|
|
72
|
-
[
|
|
72
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '1',
|
|
73
73
|
[PI_CODING_AGENT_DIR_ENV]: '/home/u/.pi/agent',
|
|
74
74
|
},
|
|
75
75
|
}),
|
|
@@ -81,7 +81,7 @@ describe('detectEnvironment 托管合取判定', () => {
|
|
|
81
81
|
it('透传污染反例 3:仅 PI_CODING_AGENT_DIR 形态匹配(EXT_LOG 未设)→ standalone-pi,evidence 点名孤置信号', () => {
|
|
82
82
|
const r = detectEnvironment(
|
|
83
83
|
signals({
|
|
84
|
-
env: { [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.
|
|
84
|
+
env: { [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.taiji/agent' },
|
|
85
85
|
}),
|
|
86
86
|
)
|
|
87
87
|
expect(r.kind).toBe('standalone-pi')
|
|
@@ -92,13 +92,13 @@ describe('detectEnvironment 托管合取判定', () => {
|
|
|
92
92
|
expect(joined).toContain(PI_CODING_AGENT_DIR_ENV)
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
-
it('
|
|
95
|
+
it('TAIJI_AGENT_EXT_LOG 非 1 值(如 0 / true)不构成命中', () => {
|
|
96
96
|
for (const value of ['0', 'true', '']) {
|
|
97
97
|
const r = detectEnvironment(
|
|
98
98
|
signals({
|
|
99
99
|
env: {
|
|
100
|
-
[
|
|
101
|
-
[PI_CODING_AGENT_DIR_ENV]: '/Users/u/.
|
|
100
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: value,
|
|
101
|
+
[PI_CODING_AGENT_DIR_ENV]: '/Users/u/.taiji/agent',
|
|
102
102
|
},
|
|
103
103
|
}),
|
|
104
104
|
)
|
|
@@ -106,12 +106,12 @@ describe('detectEnvironment 托管合取判定', () => {
|
|
|
106
106
|
}
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
-
it('形态防误报:无点前缀
|
|
110
|
-
for (const dir of ['/home/u/
|
|
109
|
+
it('形态防误报:无点前缀 taiji、嵌套浅段不匹配', () => {
|
|
110
|
+
for (const dir of ['/home/u/taiji/agent', '/home/u/.foo/agent', '/.pi/agent']) {
|
|
111
111
|
const r = detectEnvironment(
|
|
112
112
|
signals({
|
|
113
113
|
env: {
|
|
114
|
-
[
|
|
114
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '1',
|
|
115
115
|
[PI_CODING_AGENT_DIR_ENV]: dir,
|
|
116
116
|
},
|
|
117
117
|
}),
|
|
@@ -144,8 +144,8 @@ describe('detectEnvironment distribution(bundle 路径三态)', () => {
|
|
|
144
144
|
|
|
145
145
|
it('仓库路径(源码与 dev staged 资源根两种)→ dev,staged 根不被误判 packaged', () => {
|
|
146
146
|
for (const url of [
|
|
147
|
-
'file:///Users/u/Code/
|
|
148
|
-
'file:///Users/u/Code/
|
|
147
|
+
'file:///Users/u/Code/taiji/extensions/universal/session-reader/src/index.ts',
|
|
148
|
+
'file:///Users/u/Code/taiji/apps/electron/resources/extensions/@zhushanwen/pi-session-reader/index.mjs',
|
|
149
149
|
]) {
|
|
150
150
|
const r = detectEnvironment(signals({ bundleUrl: url }))
|
|
151
151
|
expect(r.distribution, `${url} 应判 dev`).toBe('dev')
|
|
@@ -160,48 +160,48 @@ describe('detectEnvironment distribution(bundle 路径三态)', () => {
|
|
|
160
160
|
})
|
|
161
161
|
|
|
162
162
|
describe('detectEnvironment dataDir 推导', () => {
|
|
163
|
-
it('托管:
|
|
163
|
+
it('托管:TAIJI_AGENT_DATA_DIR 优先于 PI_CODING_AGENT_DIR 剥层', () => {
|
|
164
164
|
const r = detectEnvironment(
|
|
165
165
|
signals({
|
|
166
166
|
env: {
|
|
167
|
-
[
|
|
168
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
169
|
-
[
|
|
167
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '1',
|
|
168
|
+
[PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/pi/agent',
|
|
169
|
+
[TAIJI_AGENT_DATA_DIR_ENV]: '/data/.taiji',
|
|
170
170
|
},
|
|
171
171
|
}),
|
|
172
172
|
)
|
|
173
|
-
expect(r.kind).toBe('
|
|
174
|
-
expect(r.dataDir).toBe('/data/.
|
|
175
|
-
expect(r.evidence.join('\n')).toContain(`来源:${
|
|
173
|
+
expect(r.kind).toBe('taiji')
|
|
174
|
+
expect(r.dataDir).toBe('/data/.taiji')
|
|
175
|
+
expect(r.evidence.join('\n')).toContain(`来源:${TAIJI_AGENT_DATA_DIR_ENV}`)
|
|
176
176
|
})
|
|
177
177
|
|
|
178
178
|
it('托管:无 DATA_DIR 时按布局剥层——新布局剥一层、旧布局剥两层', () => {
|
|
179
179
|
const newLayout = detectEnvironment(
|
|
180
180
|
signals({
|
|
181
181
|
env: {
|
|
182
|
-
[
|
|
183
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
182
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '1',
|
|
183
|
+
[PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/agent',
|
|
184
184
|
},
|
|
185
185
|
}),
|
|
186
186
|
)
|
|
187
|
-
expect(newLayout.dataDir).toBe('/data/.
|
|
187
|
+
expect(newLayout.dataDir).toBe('/data/.taiji')
|
|
188
188
|
|
|
189
189
|
const oldLayout = detectEnvironment(
|
|
190
190
|
signals({
|
|
191
191
|
env: {
|
|
192
|
-
[
|
|
193
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
192
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '1',
|
|
193
|
+
[PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/pi/agent',
|
|
194
194
|
},
|
|
195
195
|
}),
|
|
196
196
|
)
|
|
197
|
-
expect(oldLayout.dataDir).toBe('/data/.
|
|
197
|
+
expect(oldLayout.dataDir).toBe('/data/.taiji')
|
|
198
198
|
})
|
|
199
199
|
|
|
200
|
-
it('standalone:即使
|
|
200
|
+
it('standalone:即使 TAIJI_AGENT_DATA_DIR 被设置也恒 undefined', () => {
|
|
201
201
|
const r = detectEnvironment(
|
|
202
202
|
signals({
|
|
203
203
|
env: {
|
|
204
|
-
[
|
|
204
|
+
[TAIJI_AGENT_DATA_DIR_ENV]: '/data/.taiji',
|
|
205
205
|
},
|
|
206
206
|
}),
|
|
207
207
|
)
|
|
@@ -215,15 +215,15 @@ describe('detectEnvironment evidence', () => {
|
|
|
215
215
|
['全信号缺失', signals({})],
|
|
216
216
|
['托管全信号', signals({
|
|
217
217
|
env: {
|
|
218
|
-
[
|
|
219
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
220
|
-
[
|
|
218
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '1',
|
|
219
|
+
[PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/agent',
|
|
220
|
+
[TAIJI_AGENT_DATA_DIR_ENV]: '/data/.taiji',
|
|
221
221
|
},
|
|
222
222
|
bundleUrl:
|
|
223
223
|
'file:///Applications/TaiJi.app/Contents/Resources/extensions/@zhushanwen/pi-session-reader/index.mjs',
|
|
224
224
|
})],
|
|
225
|
-
['EXT_LOG 孤置', signals({ env: { [
|
|
226
|
-
['agentDir 孤置', signals({ env: { [PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
225
|
+
['EXT_LOG 孤置', signals({ env: { [TAIJI_AGENT_EXT_LOG_ENV]: '1' } })],
|
|
226
|
+
['agentDir 孤置', signals({ env: { [PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/agent' } })],
|
|
227
227
|
['bundleUrl 缺失', signals({})],
|
|
228
228
|
]
|
|
229
229
|
|
|
@@ -238,20 +238,20 @@ describe('detectEnvironment evidence', () => {
|
|
|
238
238
|
const r = detectEnvironment(
|
|
239
239
|
signals({
|
|
240
240
|
env: {
|
|
241
|
-
[
|
|
242
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
241
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '0',
|
|
242
|
+
[PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/pi/agent',
|
|
243
243
|
},
|
|
244
|
-
agentDir: '/data/.
|
|
244
|
+
agentDir: '/data/.taiji/pi/agent',
|
|
245
245
|
}),
|
|
246
246
|
)
|
|
247
247
|
const joined = r.evidence.join('\n')
|
|
248
248
|
// 未命中信号:原文 + 未命中注记
|
|
249
|
-
expect(joined).toContain(`${
|
|
249
|
+
expect(joined).toContain(`${TAIJI_AGENT_EXT_LOG_ENV}='0' → 未命中`)
|
|
250
250
|
// 命中信号:原文 + 匹配注记
|
|
251
|
-
expect(joined).toContain(`${PI_CODING_AGENT_DIR_ENV}='/data/.
|
|
251
|
+
expect(joined).toContain(`${PI_CODING_AGENT_DIR_ENV}='/data/.taiji/pi/agent'`)
|
|
252
252
|
expect(joined).toContain('形态匹配')
|
|
253
253
|
// 第三条输入:agentDir 透传记录
|
|
254
|
-
expect(joined).toContain(`agentDir='/data/.
|
|
254
|
+
expect(joined).toContain(`agentDir='/data/.taiji/pi/agent'`)
|
|
255
255
|
// 判定结论行
|
|
256
256
|
expect(joined).toContain('standalone-pi')
|
|
257
257
|
})
|
|
@@ -596,6 +596,12 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
596
596
|
const tree2 = await buildExecutionTree(MAIN, dir)
|
|
597
597
|
expect(tree2.root.sessionFile).toBeUndefined()
|
|
598
598
|
expect(tree2.totalNodes).toBe(1)
|
|
599
|
+
|
|
600
|
+
// 空串哨兵归一:mainSessionFile=''(zcode family 路由反查未命中的降级形态)与
|
|
601
|
+
// 不传等价——root 节点 details 不携带 sessionFile:"" 这类非路径占位值
|
|
602
|
+
const tree3 = await buildExecutionTree(MAIN, dir, '')
|
|
603
|
+
expect(tree3.root.sessionFile).toBeUndefined()
|
|
604
|
+
expect(tree3.totalNodes).toBe(1)
|
|
599
605
|
})
|
|
600
606
|
|
|
601
607
|
it('TC-m3b-max-depth:21+ 层 parentRecordId 链在 MAX_DEPTH(20) 截断,truncated=true 不抛错(MF-4)', async () => {
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
6
|
+
|
|
7
|
+
import { buildFamilyIndex, resolveFamily, buildZcodeSubagentsByRoot } from '../core/family.js'
|
|
8
|
+
import type { Entry, ZcodeFamilyNode } from '../core/family.js'
|
|
9
|
+
import { listZcodeManifests } from '../discovery/zcode-manifest.js'
|
|
10
|
+
|
|
11
|
+
// ============================================================
|
|
12
|
+
// family zcode 分支(design session-reader-shared-core §3.3 D5-1 / §4 场景 2)。
|
|
13
|
+
//
|
|
14
|
+
// 判据(两项、不开库):锚可解析(枚举谓词已过)∧ 库文件存在(stat)→ cleanedUp=false;
|
|
15
|
+
// 缺一 → true(GC 语义,不报错不抛异常)。刻意不含「库内 session 查询」第三项——
|
|
16
|
+
// 静息态 CANTOPEN 会误判活会话 + 列表视图不开库;反向断言(库文件在但库内 session
|
|
17
|
+
// 行被删 → 仍 false)即「列表视图不开库」的分工声明断言。
|
|
18
|
+
//
|
|
19
|
+
// 与 pi 判据不同构(D5-1 显式声明):pi = fileStats JSONL 存在性;zcode 节点无
|
|
20
|
+
// JSONL 文件,按「库可达」判。pi 侧既有 fixture 行为零变化由 family.test.ts 全绿守卫
|
|
21
|
+
// (本文件零改动 family.test.ts——更强的零涟漪证据)。
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
const ROOT = 'root-session-1'
|
|
25
|
+
|
|
26
|
+
let dir: string
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
dir = mkdtempSync(join(tmpdir(), 'family-zcode-test-'))
|
|
30
|
+
})
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
function header(id: string): Entry {
|
|
36
|
+
return { type: 'session', id, parentId: null, cwd: '/proj' }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** zcode 节点(core 层判据输入,与 ZcodeManifestRecord 结构兼容) */
|
|
40
|
+
function zcodeNode(opts: Partial<ZcodeFamilyNode> & { id: string }): ZcodeFamilyNode {
|
|
41
|
+
return {
|
|
42
|
+
rootSessionId: ROOT,
|
|
43
|
+
slug: 'worker',
|
|
44
|
+
dbPath: join(dir, 'db.sqlite'),
|
|
45
|
+
dbFileExists: false,
|
|
46
|
+
...opts,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 在 <agentDir>/subagents/<slug>/records/ 下写 zcode manifest(F21 磁盘真实形态:无 sessionFile 键) */
|
|
51
|
+
function writeZcodeManifest(
|
|
52
|
+
agentDir: string,
|
|
53
|
+
saId: string,
|
|
54
|
+
sessionRef: Record<string, string>,
|
|
55
|
+
): void {
|
|
56
|
+
const recordsDir = join(agentDir, 'subagents', 'proj-a', 'records')
|
|
57
|
+
mkdirSync(recordsDir, { recursive: true })
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(recordsDir, `${saId}.json`),
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
id: saId,
|
|
62
|
+
rootSessionId: ROOT,
|
|
63
|
+
agentName: 'worker',
|
|
64
|
+
status: 'running',
|
|
65
|
+
engine: 'zcode',
|
|
66
|
+
engineHandle: { sessionRef, poolKey: 'shared' },
|
|
67
|
+
}),
|
|
68
|
+
'utf8',
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 造一个真实 sqlite 库(session/message/part 三级表先例形态)并插入 sess 行 */
|
|
73
|
+
function createFixtureDb(dbPath: string, sessionId: string): void {
|
|
74
|
+
const db = new DatabaseSync(dbPath)
|
|
75
|
+
try {
|
|
76
|
+
db.exec('CREATE TABLE session (id TEXT PRIMARY KEY, title TEXT)')
|
|
77
|
+
db.exec('CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, sequence INTEGER)')
|
|
78
|
+
db.exec('CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT)')
|
|
79
|
+
db.prepare('INSERT INTO session (id, title) VALUES (?, ?)').run(sessionId, 'fixture')
|
|
80
|
+
} finally {
|
|
81
|
+
db.close()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('core:buildZcodeSubagentsByRoot / buildFamilyIndex 的 zcode 分支(D5-1 两态)', () => {
|
|
86
|
+
it('库文件存在 → cleanedUp=false;库文件缺席 → cleanedUp=true(缺一即 true,不抛错)', () => {
|
|
87
|
+
const index = buildFamilyIndex([header(ROOT)], [], new Map(), [
|
|
88
|
+
zcodeNode({ id: 'sa-alive', dbFileExists: true }),
|
|
89
|
+
zcodeNode({ id: 'sa-gone', dbFileExists: false }),
|
|
90
|
+
])
|
|
91
|
+
const family = resolveFamily(ROOT, index)
|
|
92
|
+
|
|
93
|
+
const alive = family.subagents.find((s) => s.sessionId === 'sa-alive')
|
|
94
|
+
const gone = family.subagents.find((s) => s.sessionId === 'sa-gone')
|
|
95
|
+
expect(alive?.cleanedUp).toBe(false)
|
|
96
|
+
expect(gone?.cleanedUp).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('zcode 节点按 rootSessionId 挂载,隔代场景与 pi 同链合并(resolveFamily 消费同一 subagentsByRoot)', () => {
|
|
100
|
+
const FORK = 'fork-session-1'
|
|
101
|
+
const headers = [header(ROOT), { ...header(FORK), parentSession: `/s/_${ROOT}.jsonl` }]
|
|
102
|
+
const index = buildFamilyIndex(headers, [], new Map(), [
|
|
103
|
+
zcodeNode({ id: 'sa-deep', rootSessionId: FORK }),
|
|
104
|
+
])
|
|
105
|
+
// Q1 隔代:zcode subagent 挂在 fork 子代下,从 ROOT resolve 仍可见
|
|
106
|
+
expect(resolveFamily(ROOT, index).subagents.map((s) => s.sessionId)).toContain('sa-deep')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('不传 zcodeNodes / 传空数组 → pi 行为不变(零涟漪等价)', () => {
|
|
110
|
+
const piIdent: Entry = {
|
|
111
|
+
type: 'custom',
|
|
112
|
+
id: 'sa-pi',
|
|
113
|
+
parentId: null,
|
|
114
|
+
customType: 'subagent-identity',
|
|
115
|
+
data: { rootSessionId: ROOT, slug: 'pi-sub' },
|
|
116
|
+
}
|
|
117
|
+
const stats = new Map([[ROOT, { mtime: 1, size: 2 }], ['sa-pi', { mtime: 3, size: 4 }]])
|
|
118
|
+
const without = buildFamilyIndex([header(ROOT)], [piIdent], stats)
|
|
119
|
+
const withEmpty = buildFamilyIndex([header(ROOT)], [piIdent], stats, [])
|
|
120
|
+
const a = resolveFamily(ROOT, without)
|
|
121
|
+
const b = resolveFamily(ROOT, withEmpty)
|
|
122
|
+
expect(b.subagents).toEqual(a.subagents)
|
|
123
|
+
expect(b.subagents[0]?.cleanedUp).toBe(false) // pi 判据:fileStats 命中
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('zcode 节点与 pi 孤儿节点并存:判据互不污染(不同构声明断言)', () => {
|
|
127
|
+
// pi 孤儿:identity 在但 fileStats 不含 → cleanedUp=true(JSONL GC)
|
|
128
|
+
const piOrphan: Entry = {
|
|
129
|
+
type: 'custom',
|
|
130
|
+
id: 'sa-pi-orphan',
|
|
131
|
+
parentId: null,
|
|
132
|
+
customType: 'subagent-identity',
|
|
133
|
+
data: { rootSessionId: ROOT, slug: 'orphan' },
|
|
134
|
+
}
|
|
135
|
+
const index = buildFamilyIndex([header(ROOT)], [piOrphan], new Map(), [
|
|
136
|
+
zcodeNode({ id: 'sa-zcode-gone', dbFileExists: false }),
|
|
137
|
+
])
|
|
138
|
+
const subs = resolveFamily(ROOT, index).subagents
|
|
139
|
+
// 两节点都 cleanedUp=true,但判据来源不同:pi 走 fileStats,zcode 走 dbFileExists
|
|
140
|
+
expect(subs.find((s) => s.sessionId === 'sa-pi-orphan')?.cleanedUp).toBe(true)
|
|
141
|
+
expect(subs.find((s) => s.sessionId === 'sa-zcode-gone')?.cleanedUp).toBe(true)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('SubagentRef 字段映射:富字段透传、sessionFile 不设、mtime/sizeBytes 占位 0', () => {
|
|
145
|
+
const byRoot = buildZcodeSubagentsByRoot([
|
|
146
|
+
zcodeNode({
|
|
147
|
+
id: 'sa-rich',
|
|
148
|
+
slug: 'rich-slug',
|
|
149
|
+
agentName: 'explorer',
|
|
150
|
+
task: 'do research',
|
|
151
|
+
model: 'glm-5.3',
|
|
152
|
+
status: 'closed',
|
|
153
|
+
dbFileExists: true,
|
|
154
|
+
}),
|
|
155
|
+
])
|
|
156
|
+
const ref = byRoot.get(ROOT)?.[0]
|
|
157
|
+
expect(ref?.slug).toBe('rich-slug')
|
|
158
|
+
expect(ref?.agentName).toBe('explorer')
|
|
159
|
+
expect(ref?.task).toBe('do research')
|
|
160
|
+
expect(ref?.model).toBe('glm-5.3')
|
|
161
|
+
expect(ref?.status).toBe('closed')
|
|
162
|
+
expect(ref?.sessionFile).toBeUndefined() // zcode 无 session.jsonl
|
|
163
|
+
expect(ref?.mtime).toBe(0)
|
|
164
|
+
expect(ref?.sizeBytes).toBe(0)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(场景 2 两态 + 反向断言)', () => {
|
|
169
|
+
it('库可达态(dbPath 指向真实 fixture 库)→ cleanedUp=false', async () => {
|
|
170
|
+
const dbPath = join(dir, 'isolated.sqlite')
|
|
171
|
+
createFixtureDb(dbPath, 'sess_live')
|
|
172
|
+
writeZcodeManifest(dir, 'sa-live', { sessionId: 'sess_live', dbPath })
|
|
173
|
+
|
|
174
|
+
const nodes = await listZcodeManifests(dir)
|
|
175
|
+
expect(nodes).toHaveLength(1)
|
|
176
|
+
expect(nodes[0]?.dbFileExists).toBe(true)
|
|
177
|
+
|
|
178
|
+
const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))
|
|
179
|
+
const sub = family.subagents.find((s) => s.sessionId === 'sa-live')
|
|
180
|
+
expect(sub?.cleanedUp).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('库不存在态(dbPath 指向不存在路径)→ cleanedUp=true(GC 语义,不报错)', async () => {
|
|
184
|
+
writeZcodeManifest(dir, 'sa-gone', {
|
|
185
|
+
sessionId: 'sess_gone',
|
|
186
|
+
dbPath: join(dir, 'subagents', 'proj-a', 'missing.sqlite'),
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const nodes = await listZcodeManifests(dir)
|
|
190
|
+
expect(nodes[0]?.dbFileExists).toBe(false)
|
|
191
|
+
|
|
192
|
+
const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))
|
|
193
|
+
expect(family.subagents.find((s) => s.sessionId === 'sa-gone')?.cleanedUp).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('反向断言:库文件在但库内 session 行被删 → 仍 cleanedUp=false(列表视图不开库的分工声明)', async () => {
|
|
197
|
+
// 分工(D5-1):库内 session 已被 zcode GC 的情形由 read 路径 zcode_session_not_found
|
|
198
|
+
// 权威承接;family 只 stat 文件存在性。本用例删行留文件,断言 family 不受库内容影响。
|
|
199
|
+
const dbPath = join(dir, 'gc.sqlite')
|
|
200
|
+
createFixtureDb(dbPath, 'sess_victim')
|
|
201
|
+
writeZcodeManifest(dir, 'sa-gced', { sessionId: 'sess_victim', dbPath })
|
|
202
|
+
|
|
203
|
+
// 前置核实:fixture 库里确实有该 session 行(反向断言的非平凡性前提)
|
|
204
|
+
{
|
|
205
|
+
const db = new DatabaseSync(dbPath)
|
|
206
|
+
try {
|
|
207
|
+
const row = db.prepare('SELECT COUNT(*) AS c FROM session WHERE id = ?').get('sess_victim') as
|
|
208
|
+
| { c: number }
|
|
209
|
+
| undefined
|
|
210
|
+
expect(row?.c).toBe(1)
|
|
211
|
+
} finally {
|
|
212
|
+
db.close()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 删库内 session 行(含 message/part 级联),db 文件保留
|
|
217
|
+
{
|
|
218
|
+
const db = new DatabaseSync(dbPath)
|
|
219
|
+
try {
|
|
220
|
+
db.prepare('DELETE FROM part WHERE session_id = ?').run('sess_victim')
|
|
221
|
+
db.prepare('DELETE FROM message WHERE session_id = ?').run('sess_victim')
|
|
222
|
+
db.prepare('DELETE FROM session WHERE id = ?').run('sess_victim')
|
|
223
|
+
} finally {
|
|
224
|
+
db.close()
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const nodes = await listZcodeManifests(dir)
|
|
229
|
+
expect(nodes[0]?.dbFileExists).toBe(true) // 只 stat:文件在即 true,不看内容
|
|
230
|
+
|
|
231
|
+
const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))
|
|
232
|
+
expect(family.subagents.find((s) => s.sessionId === 'sa-gced')?.cleanedUp).toBe(false)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('坏 manifest 与缺库节点混排 → 好节点照常输出(单节点不可达不炸整屏)', async () => {
|
|
236
|
+
const dbPath = join(dir, 'ok.sqlite')
|
|
237
|
+
createFixtureDb(dbPath, 'sess_ok')
|
|
238
|
+
writeZcodeManifest(dir, 'sa-ok', { sessionId: 'sess_ok', dbPath })
|
|
239
|
+
// 缺库节点:合法 zcode 形态(dbFileExists=false,不抛错),不剔除出列表
|
|
240
|
+
writeZcodeManifest(dir, 'sa-broken-manifest', { sessionId: 'sess_x', dbPath: '/nonexistent/x.sqlite' })
|
|
241
|
+
// 坏 JSON manifest:枚举跳过(不中断)
|
|
242
|
+
writeFileSync(
|
|
243
|
+
join(dir, 'subagents', 'proj-a', 'records', 'sa-corrupt.json'),
|
|
244
|
+
'{{{broken',
|
|
245
|
+
'utf8',
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
const nodes = await listZcodeManifests(dir)
|
|
249
|
+
expect(nodes.map((n) => n.id).sort()).toEqual(['sa-broken-manifest', 'sa-ok'])
|
|
250
|
+
|
|
251
|
+
const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))
|
|
252
|
+
expect(family.subagents.find((s) => s.sessionId === 'sa-ok')?.cleanedUp).toBe(false)
|
|
253
|
+
expect(family.subagents.find((s) => s.sessionId === 'sa-broken-manifest')?.cleanedUp).toBe(true)
|
|
254
|
+
})
|
|
255
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import { buildFamilyIndex, resolveFamily } from '../core/family.js'
|
|
3
|
-
import type { Entry } from '
|
|
3
|
+
import type { Entry } from '@zhushanwen/session-core'
|
|
4
4
|
|
|
5
5
|
// ---- fixture 常量(映射 design §3.3 D-7 Q1 真实场景)----
|
|
6
6
|
const ROOT = '019fe620' // 家族根(无 parentSession)
|
|
@@ -186,6 +186,29 @@ describe('findSessions', () => {
|
|
|
186
186
|
expect(matches[0].cwd).toBe('/proj-a')
|
|
187
187
|
})
|
|
188
188
|
|
|
189
|
+
it('缺 cwd 的 header 进候选集(谓词不统一负面锚,design §4 负面行为验证)', async () => {
|
|
190
|
+
// header 行只有 id(无 cwd/timestamp):runtime 导入侧「id+cwd 非空」严谓词会拒收,
|
|
191
|
+
// reader 宽谓词(仅要求 type==='session' ∧ id 为 string)必须照常收录——基座化
|
|
192
|
+
// (readFirstJsonlLine 改指 session-core)不得顺手统一谓词,本用例即机器锚。
|
|
193
|
+
const headerPath = join(slugDir, 'no-cwd.jsonl')
|
|
194
|
+
await mkdir(slugDir, { recursive: true })
|
|
195
|
+
await writeFile(headerPath, JSON.stringify({ type: 'session', id: '019e0001-no-cwd' }) + '\n')
|
|
196
|
+
await makeSession(slugDir, { name: 'with-cwd.jsonl', id: '019e0002-with-cwd', cwd: '/demo' })
|
|
197
|
+
|
|
198
|
+
// find 结果:缺 cwd header 命中,cwd 归一化为空串(buildCandidate 的 header.cwd ?? '')
|
|
199
|
+
const { matches } = await findSessions('no-cwd', agentDir)
|
|
200
|
+
expect(matches).toHaveLength(1)
|
|
201
|
+
expect(matches[0].sessionId).toBe('019e0001-no-cwd')
|
|
202
|
+
expect(matches[0].cwd).toBe('')
|
|
203
|
+
|
|
204
|
+
// 候选枚举(recent 路径枚举全部候选):缺 cwd header 未被候选集排除
|
|
205
|
+
const recent = await findSessions('recent', agentDir)
|
|
206
|
+
expect(recent.matches.map((m) => m.sessionId).sort()).toEqual([
|
|
207
|
+
'019e0001-no-cwd',
|
|
208
|
+
'019e0002-with-cwd',
|
|
209
|
+
])
|
|
210
|
+
})
|
|
211
|
+
|
|
189
212
|
it('limit 截断:truncated 标记正确', async () => {
|
|
190
213
|
// 5 个文件 id 都含 "common",uuid 片段 "common" 全匹配
|
|
191
214
|
for (let i = 0; i < 5; i++) {
|
|
@@ -578,7 +601,7 @@ describe('findSessions', () => {
|
|
|
578
601
|
describe('u11 标题元数据(metadataProvider 注入,§6.6)', () => {
|
|
579
602
|
let root: string
|
|
580
603
|
let agentDir: string
|
|
581
|
-
/** 平铺 main 根(
|
|
604
|
+
/** 平铺 main 根(taiji 形态:session 直在 sessions/ 下,[default] 根无子目录) */
|
|
582
605
|
let flatMain: string
|
|
583
606
|
/** legacy 根(dirname(agentDir)/sessions) */
|
|
584
607
|
let legacyDir: string
|
|
@@ -169,7 +169,7 @@ describe('sessionReaderExtension - execute 信号包采集(U3:ctx 全形态
|
|
|
169
169
|
})
|
|
170
170
|
|
|
171
171
|
it('u11:execute 构造 metadataProvider 接 SessionManager.listAll——keyword 标题命中端到端', async () => {
|
|
172
|
-
// fixture:平铺 main 根(
|
|
172
|
+
// fixture:平铺 main 根(taiji 形态),session 首消息不含 query,标题经 mock listAll 注入
|
|
173
173
|
const tmp = await mkdtemp(join(tmpdir(), 'index-u11-'))
|
|
174
174
|
try {
|
|
175
175
|
const agentDir = join(tmp, 'agent')
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { parseSessionContent, parseSessionFile } from '
|
|
3
|
-
import type { Entry } from '../core/parser.js'
|
|
2
|
+
import { parseSessionContent, parseSessionFile, type Entry } from '@zhushanwen/session-core'
|
|
4
3
|
import { REAL_SESSION, HAS_REAL_SESSION } from './real-data.js'
|
|
5
4
|
|
|
6
5
|
/** 构造单行 JSONL entry 字符串 */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { execSync } from 'node:child_process'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
3
4
|
import { join } from 'node:path'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -13,8 +14,8 @@ import { join } from 'node:path'
|
|
|
13
14
|
* 本文件无 .test.ts 后缀,vitest include 只收集 `src/__tests__/` 下以 .test.ts 结尾的文件,不收集本文件。
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
/** 真实 pi agent
|
|
17
|
-
export const REAL_AGENT_DIR = '
|
|
17
|
+
/** 真实 pi agent 目录(pi 官方家目录布局,随宿主 homedir 推导),用于集成测试。 */
|
|
18
|
+
export const REAL_AGENT_DIR = join(homedir(), '.pi', 'agent')
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* 全量扫描类真实数据用例的统一超时标定(90s)。
|
|
@@ -31,13 +32,25 @@ export const E6 = '019e6c96-0a0c-74b8-a73f-d1854d88e2a7'
|
|
|
31
32
|
/** 真实 fork 家族根(fork 子代 019fe632,隔代 subagent 019fe635 挂在 019fe632 下)。 */
|
|
32
33
|
export const FAM = '019fe620-8ae1-78a7-b76a-43a1ba4cc3c7'
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
35
|
+
/**
|
|
36
|
+
* E6 的完整文件路径(parser/turns/render 直接 parseSessionFile 用)。
|
|
37
|
+
* 按 sid 运行时定位而非硬编码宿主 workspace 目录名:真实文件挂在哪个 slug 目录随采集机器而异,
|
|
38
|
+
* 硬编码会绑死本机路径形态;定位不到时回落相对布局,由下方 HAS_REAL_SESSION 守卫 skip。
|
|
39
|
+
*/
|
|
40
|
+
export const REAL_SESSION = (() => {
|
|
41
|
+
const fallback = join(REAL_AGENT_DIR, 'sessions', `2026-05-28T03-17-12-844Z_${E6}.jsonl`)
|
|
42
|
+
if (!existsSync(REAL_AGENT_DIR)) return fallback
|
|
43
|
+
try {
|
|
44
|
+
return (
|
|
45
|
+
execSync(
|
|
46
|
+
`find ${REAL_AGENT_DIR}/sessions -name '*${E6}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
47
|
+
{ encoding: 'utf8' },
|
|
48
|
+
).trim() || fallback
|
|
49
|
+
)
|
|
50
|
+
} catch {
|
|
51
|
+
return fallback
|
|
52
|
+
}
|
|
53
|
+
})()
|
|
41
54
|
|
|
42
55
|
/** 同步探测真实 session 文件是否存在(不存在则 skip,避免在无该数据的机器上硬失败)。 */
|
|
43
56
|
export function hasRealSession(sid: string): boolean {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import { renderOutline, renderExpand, renderDetail, type ToolResultSummaryEntry } from '../core/render.js'
|
|
3
|
-
import type
|
|
3
|
+
import { parseSessionFile, 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 { parseSessionFile } from '../core/parser.js'
|
|
7
6
|
import { buildTreeView } from '../core/tree.js'
|
|
8
7
|
import { segmentTurns } from '../core/turns.js'
|
|
9
8
|
import { REAL_SESSION, HAS_REAL_SESSION } from './real-data.js'
|