@zhushanwen/pi-session-reader 0.13.1 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/__tests__/entry-anchor.test.ts +704 -324
- package/src/__tests__/family.test.ts +588 -312
- package/src/__tests__/{cross-package-subagent-core.test.ts → path-encoding.test.ts} +194 -30
- package/src/__tests__/roots-merged.test.ts +4919 -0
- package/src/__tests__/roots.test.ts +1986 -432
- package/src/__tests__/tool-handler.test.ts +872 -3216
- package/src/__tests__/tree.test.ts +738 -101
- package/src/__tests__/archive-ledger.test.ts +0 -447
- package/src/__tests__/execution-tree.test.ts +0 -898
- package/src/__tests__/family-zcode.test.ts +0 -265
- package/src/__tests__/find.test.ts +0 -802
- package/src/__tests__/rebuild-manifest-dual-write.test.ts +0 -188
- package/src/__tests__/render.test.ts +0 -481
- package/src/__tests__/result.test.ts +0 -347
- package/src/__tests__/subagents.test.ts +0 -668
- package/src/__tests__/summary.test.ts +0 -223
- package/src/__tests__/turns.test.ts +0 -144
- package/src/__tests__/zcode-manifest.test.ts +0 -372
- package/src/__tests__/zcode-routing.test.ts +0 -772
|
@@ -1,772 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import { mkdtempSync, writeFileSync, mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs'
|
|
4
|
-
import { join, dirname } from 'node:path'
|
|
5
|
-
import { DatabaseSync } from 'node:sqlite'
|
|
6
|
-
import { Check } from 'typebox/value'
|
|
7
|
-
|
|
8
|
-
import { handleSessionRead } from '../tool-handler.js'
|
|
9
|
-
import {
|
|
10
|
-
SQLITE_DRIVER_UNSUPPORTED_MARK,
|
|
11
|
-
ZCODE_DB_MISSING_MARK,
|
|
12
|
-
zcodeReadErrorMessage,
|
|
13
|
-
} from '../tool-format.js'
|
|
14
|
-
import { SqliteUnreadableError } from '@zhushanwen/zcode-session-source'
|
|
15
|
-
import sessionReaderExtension from '../index.js'
|
|
16
|
-
import { setPiHandle } from '@zhushanwen/pi-extension-logger'
|
|
17
|
-
|
|
18
|
-
// ============================================================
|
|
19
|
-
// U9 路由 + 白名单闸 + 错误范围集成测试(design session-reader-shared-core
|
|
20
|
-
// §3.1 三层定位链 / §3.4 七码 + 检查顺序三段递进 / §4 场景 1a 变体 M/E +
|
|
21
|
-
// 场景 2 family + 场景 5 八类失败 + 场景 3 pi 零回归 + D4 schema 零变化)。
|
|
22
|
-
//
|
|
23
|
-
// fixture 骨架(mkdtemp 自建自删):root/agent = agentDir(dirname = dataDir)、
|
|
24
|
-
// root/engines/zcode/session-db/db.sqlite = 隔离库(白名单精确命中)、
|
|
25
|
-
// root/live-sessions = liveSessionDir(entry 兜底候选根)、
|
|
26
|
-
// root/agent/sessions = main 默认根(family/rootSession 视图的 header 来源)。
|
|
27
|
-
//
|
|
28
|
-
// 行集非平凡性:sess_aaa(ZASENTINEL)与 sess_bbb(ZBSENTINEL + ZZQFIXTURE +
|
|
29
|
-
// toolCall 对 + compaction part)两个可区分行集——「取首条/合并/读错行集」的
|
|
30
|
-
// 实现会直接失败。manifest 锚与 entry 锚刻意指向不同 session(变体 M 的
|
|
31
|
-
// instrument:读到 manifest 的 sess_bbb 而非 entry 的 sess_aaa = 走对主路径)。
|
|
32
|
-
// ============================================================
|
|
33
|
-
|
|
34
|
-
const ISOLATED_DB_SEGMENTS = ['engines', 'zcode', 'session-db', 'db.sqlite']
|
|
35
|
-
|
|
36
|
-
const SA_M = 'sa-fix1' // 变体 M / 八类失败主 sa-id
|
|
37
|
-
const SA_E = 'sa-fix2' // 变体 E(manifest 缺位,entry 兜底)
|
|
38
|
-
const SESS_A = 'sess_aaa'
|
|
39
|
-
const SESS_B = 'sess_bbb'
|
|
40
|
-
const ROOT_SESSION = 'root-session-1'
|
|
41
|
-
const BASE_MS = 1758379200000
|
|
42
|
-
|
|
43
|
-
let root: string
|
|
44
|
-
let agentDir: string
|
|
45
|
-
let dbPath: string
|
|
46
|
-
let liveDir: string
|
|
47
|
-
let appendEntries: ReturnType<typeof vi.fn>
|
|
48
|
-
|
|
49
|
-
beforeEach(() => {
|
|
50
|
-
root = mkdtempSync(join(tmpdir(), 'zcode-routing-test-'))
|
|
51
|
-
agentDir = join(root, 'agent')
|
|
52
|
-
liveDir = join(root, 'live-sessions')
|
|
53
|
-
mkdirSync(agentDir, { recursive: true })
|
|
54
|
-
mkdirSync(liveDir, { recursive: true })
|
|
55
|
-
dbPath = join(root, ...ISOLATED_DB_SEGMENTS)
|
|
56
|
-
// 结构化日志捕获:logger.warn 走 appendEntry 通道(不进 LLM 可见部分——本文件同时
|
|
57
|
-
// 是「降级留痕不进 LLM 可见部分」契约的断言范围)。测试后清注入防跨文件泄漏。
|
|
58
|
-
appendEntries = vi.fn()
|
|
59
|
-
setPiHandle({ appendEntry: appendEntries } as unknown as Parameters<typeof setPiHandle>[0])
|
|
60
|
-
})
|
|
61
|
-
afterEach(() => {
|
|
62
|
-
setPiHandle(undefined)
|
|
63
|
-
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// fixture builders
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
interface SessionRows {
|
|
71
|
-
id: string
|
|
72
|
-
title: string
|
|
73
|
-
messages: Array<{ id: string; role: string; parts: Array<Record<string, unknown>> }>
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** sess_aaa 行集:两轮对话,文本带 ZASENTINEL(entry 首条锚指向它)。 */
|
|
77
|
-
function sessARows(): SessionRows {
|
|
78
|
-
return {
|
|
79
|
-
id: SESS_A,
|
|
80
|
-
title: 'sess A title',
|
|
81
|
-
messages: [
|
|
82
|
-
userMsg('m1', 0, '第一轮用户提问 ZASENTINEL 锚'),
|
|
83
|
-
assistantMsg('m2', 1, ['第一轮助手回答 ZASENTINEL']),
|
|
84
|
-
userMsg('m3', 2, '第二轮用户提问'),
|
|
85
|
-
assistantMsg('m4', 3, ['第二轮助手回答 ZASENTINEL']),
|
|
86
|
-
],
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** sess_bbb 行集:两轮 + toolCall 对 + ZZQFIXTURE + compaction(manifest 锚指向它)。 */
|
|
91
|
-
function sessBRows(): SessionRows {
|
|
92
|
-
return {
|
|
93
|
-
id: SESS_B,
|
|
94
|
-
title: 'sess B title',
|
|
95
|
-
messages: [
|
|
96
|
-
userMsg('m1', 0, '派发任务 ZZQFIXTURE 检索锚点'),
|
|
97
|
-
assistantMsg('m2', 1, [], {
|
|
98
|
-
toolPart: {
|
|
99
|
-
type: 'tool',
|
|
100
|
-
callID: 'tc-1',
|
|
101
|
-
tool: 'Bash',
|
|
102
|
-
state: { status: 'completed', input: { command: 'echo hi' }, output: 'hi' },
|
|
103
|
-
},
|
|
104
|
-
extraParts: [{ type: 'text', text: '工具执行完成', time: { start: BASE_MS + 20 } }],
|
|
105
|
-
}),
|
|
106
|
-
userMsg('m3', 2, '继续第二轮'),
|
|
107
|
-
assistantMsg('m4', 3, ['第二轮 ZBSENTINEL 收尾'], {
|
|
108
|
-
extraParts: [{ type: 'compaction', data: { trigger: 'auto' }, time: { start: BASE_MS + 40 } }],
|
|
109
|
-
}),
|
|
110
|
-
],
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function userMsg(id: string, seq: number, text: string): SessionRows['messages'][number] {
|
|
115
|
-
return {
|
|
116
|
-
id,
|
|
117
|
-
role: 'user',
|
|
118
|
-
parts: [{ type: 'text', text, time: { start: BASE_MS + seq * 10 } }],
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function assistantMsg(
|
|
123
|
-
id: string,
|
|
124
|
-
seq: number,
|
|
125
|
-
texts: string[],
|
|
126
|
-
opts: { toolPart?: Record<string, unknown>; extraParts?: Array<Record<string, unknown>> } = {},
|
|
127
|
-
): SessionRows['messages'][number] {
|
|
128
|
-
const parts: Array<Record<string, unknown>> = []
|
|
129
|
-
if (opts.toolPart) parts.push(opts.toolPart)
|
|
130
|
-
texts.forEach((t, i) => parts.push({ type: 'text', text: t, time: { start: BASE_MS + seq * 10 + i } }))
|
|
131
|
-
if (opts.extraParts) parts.push(...opts.extraParts)
|
|
132
|
-
parts.push({ type: 'step-finish', reason: 'stop', tokens: { input: 1, output: 1, total: 2 }, cost: 0 })
|
|
133
|
-
return { id, role: 'assistant', parts }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* 建真实 fixture 隔离库(WAL + session/message/part/schema_migration 四表,行形态 =
|
|
138
|
-
* zcode-session-source helpers 同款宿主 schema 0.16.5 消费方子集)。
|
|
139
|
-
* 测试进程跑 node(node:sqlite 可用);生产读链的驱动选择在 source 包内。
|
|
140
|
-
*/
|
|
141
|
-
function createZcodeDb(path: string, schemaVersion = '0.16.5', sessions: SessionRows[] = []): void {
|
|
142
|
-
mkdirSync(join(path, '..'), { recursive: true })
|
|
143
|
-
const db = new DatabaseSync(path)
|
|
144
|
-
try {
|
|
145
|
-
db.exec('PRAGMA journal_mode=WAL')
|
|
146
|
-
db.exec('BEGIN')
|
|
147
|
-
db.exec(
|
|
148
|
-
'CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT NOT NULL, title TEXT NOT NULL, ' +
|
|
149
|
-
"task_type TEXT NOT NULL DEFAULT 'interactive', time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL)",
|
|
150
|
-
)
|
|
151
|
-
db.exec('CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, sequence INTEGER, data TEXT NOT NULL)')
|
|
152
|
-
db.exec('CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT NOT NULL, session_id TEXT NOT NULL, sequence INTEGER, data TEXT NOT NULL)')
|
|
153
|
-
db.exec('CREATE TABLE schema_migration (id TEXT PRIMARY KEY, checksum TEXT NOT NULL, app_version TEXT, time_applied INTEGER NOT NULL)')
|
|
154
|
-
db.prepare('INSERT INTO schema_migration (id, checksum, app_version, time_applied) VALUES (?, ?, ?, ?)').run('0001_seed', 'x', schemaVersion, 1)
|
|
155
|
-
const insSession = db.prepare('INSERT INTO session (id, directory, title, task_type, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)')
|
|
156
|
-
const insMessage = db.prepare('INSERT INTO message (id, session_id, sequence, data) VALUES (?, ?, ?, ?)')
|
|
157
|
-
const insPart = db.prepare('INSERT INTO part (id, message_id, session_id, sequence, data) VALUES (?, ?, ?, ?, ?)')
|
|
158
|
-
for (const [si, s] of sessions.entries()) {
|
|
159
|
-
insSession.run(s.id, '/tmp/fix', s.title, 'interactive', BASE_MS, BASE_MS + si * 10)
|
|
160
|
-
for (const [mi, m] of s.messages.entries()) {
|
|
161
|
-
insMessage.run(`${s.id}_${m.id}`, s.id, mi, JSON.stringify({ role: m.role, time: { created: BASE_MS + mi * 10 } }))
|
|
162
|
-
for (const [pi, p] of m.parts.entries()) {
|
|
163
|
-
insPart.run(`${s.id}_${m.id}_p${pi}`, `${s.id}_${m.id}`, s.id, pi, JSON.stringify(p))
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
db.exec('COMMIT')
|
|
168
|
-
} finally {
|
|
169
|
-
db.close()
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** zcode manifest(F21 磁盘真实形态:无 sessionFile 键)。 */
|
|
174
|
-
function writeZcodeManifest(
|
|
175
|
-
saId: string,
|
|
176
|
-
opts: { sessionRef?: Record<string, string>; omitEngineHandle?: boolean; engine?: string | true } = {},
|
|
177
|
-
): void {
|
|
178
|
-
// [session-store-layout-inversion D6/单新根] records 读侧 = 存储树分区固定位置
|
|
179
|
-
// <root>/<rootSid>/subagents/records/(旧根 <agentDir>/subagents 不再读写)。
|
|
180
|
-
const recordsDir = join(dirname(agentDir), 'ext-sessions', ROOT_SESSION, 'subagents', 'records')
|
|
181
|
-
mkdirSync(recordsDir, { recursive: true })
|
|
182
|
-
const m: Record<string, unknown> = {
|
|
183
|
-
id: saId,
|
|
184
|
-
rootSessionId: ROOT_SESSION,
|
|
185
|
-
agentName: 'worker',
|
|
186
|
-
task: 'fixture task',
|
|
187
|
-
status: 'running',
|
|
188
|
-
}
|
|
189
|
-
m.engine = opts.engine === undefined ? 'zcode' : opts.engine
|
|
190
|
-
if (!opts.omitEngineHandle) {
|
|
191
|
-
m.engineHandle = {
|
|
192
|
-
sessionRef: opts.sessionRef ?? { sessionId: SESS_B, dbPath },
|
|
193
|
-
poolKey: 'shared',
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
writeFileSync(join(recordsDir, `${saId}.json`), JSON.stringify(m), 'utf8')
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** subagent-record custom entry 行(U7 entry-anchor.test 同款形状)。 */
|
|
200
|
-
function recordLine(entryId: string, saId: string, sessionRef: Record<string, string>): string {
|
|
201
|
-
return JSON.stringify({
|
|
202
|
-
type: 'custom',
|
|
203
|
-
customType: 'subagent-record',
|
|
204
|
-
id: entryId,
|
|
205
|
-
parentId: null,
|
|
206
|
-
data: {
|
|
207
|
-
v: 2, kind: 'settled', id: saId, status: 'idle', stopReason: 'completed', endedAt: BASE_MS + 1,
|
|
208
|
-
turns: 1, totalTokens: 10, engine: 'zcode', engineHandle: { sessionRef, poolKey: 'shared' },
|
|
209
|
-
},
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* pi 形态的 subagent-record custom entry 行(回归 A1 instrument):engine 键缺省 +
|
|
215
|
-
* sessionRef 无 dbPath——subagent-core record-entry.ts 的 pi 派生视图形态(engineHandle
|
|
216
|
-
* 可选、sessionRef 整体透传不枚举内部键;最小可信子集,跨包漂移由 entry-anchor.test 检查)。
|
|
217
|
-
*/
|
|
218
|
-
function piRecordLine(entryId: string, saId: string): string {
|
|
219
|
-
return JSON.stringify({
|
|
220
|
-
type: 'custom',
|
|
221
|
-
customType: 'subagent-record',
|
|
222
|
-
id: entryId,
|
|
223
|
-
parentId: null,
|
|
224
|
-
data: {
|
|
225
|
-
// 登记 §3.3:条目范围 = v2 族——pi 形态 = 注册条(身份)+ 终态条(engineHandle 半键)。
|
|
226
|
-
v: 2,
|
|
227
|
-
kind: 'settled',
|
|
228
|
-
id: saId,
|
|
229
|
-
status: 'idle',
|
|
230
|
-
stopReason: 'completed',
|
|
231
|
-
endedAt: BASE_MS + 1,
|
|
232
|
-
turns: 1,
|
|
233
|
-
totalTokens: 10,
|
|
234
|
-
engineHandle: { sessionRef: { sessionId: SESS_A }, poolKey: 'shared' },
|
|
235
|
-
},
|
|
236
|
-
})
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const HEADER_LINE = JSON.stringify({ type: 'session', id: 'main-session-1', cwd: '/proj' })
|
|
240
|
-
const USER_LINE = JSON.stringify({
|
|
241
|
-
type: 'message',
|
|
242
|
-
id: 'u1',
|
|
243
|
-
parentId: null,
|
|
244
|
-
message: { role: 'user', content: [{ type: 'text', text: 'dispatch subagent' }] },
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
/** liveSessionDir 内的主 session 文件(entry 兜底候选)。 */
|
|
248
|
-
function writeLiveMainSession(lines: string[]): void {
|
|
249
|
-
writeFileSync(join(liveDir, 'main-session.jsonl'), [HEADER_LINE, USER_LINE, ...lines].join('\n') + '\n', 'utf8')
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** main 默认根的 root session(family/rootSessionId 视图的 byId 素材)。 */
|
|
253
|
-
function writeRootSessionFile(): void {
|
|
254
|
-
const sessionsDir = join(agentDir, 'sessions')
|
|
255
|
-
mkdirSync(sessionsDir, { recursive: true })
|
|
256
|
-
writeFileSync(
|
|
257
|
-
join(sessionsDir, `1700000000000_${ROOT_SESSION}.jsonl`),
|
|
258
|
-
JSON.stringify({ type: 'session', id: ROOT_SESSION, cwd: '/proj' }) + '\n',
|
|
259
|
-
'utf8',
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** pi manifest + pi subagent session 文件(pi 穿插断言用;engine 缺省 = pi 存量形态)。 */
|
|
264
|
-
function writePiFixture(): void {
|
|
265
|
-
const subDir = join(dirname(agentDir), 'ext-sessions', ROOT_SESSION, 'subagents', 'sessions')
|
|
266
|
-
mkdirSync(subDir, { recursive: true })
|
|
267
|
-
const piSession = join(subDir, '019e6c96-pi-sub-agent-00000000004a4.jsonl')
|
|
268
|
-
writeFileSync(
|
|
269
|
-
piSession,
|
|
270
|
-
[
|
|
271
|
-
JSON.stringify({ type: 'session', id: '019e6c96-pi-sub-agent-00000000004a4', cwd: '/proj' }),
|
|
272
|
-
JSON.stringify({ type: 'message', id: 'p1', parentId: null, message: { role: 'user', content: [{ type: 'text', text: 'pi task' }] } }),
|
|
273
|
-
JSON.stringify({ type: 'message', id: 'p2', parentId: 'p1', message: { role: 'assistant', content: [{ type: 'text', text: 'PI-SENTINEL 正文内容' }] } }),
|
|
274
|
-
].join('\n') + '\n',
|
|
275
|
-
'utf8',
|
|
276
|
-
)
|
|
277
|
-
const recordsDir = join(dirname(agentDir), 'ext-sessions', ROOT_SESSION, 'subagents', 'records')
|
|
278
|
-
mkdirSync(recordsDir, { recursive: true })
|
|
279
|
-
writeFileSync(
|
|
280
|
-
join(recordsDir, 'sa-pi-1.json'),
|
|
281
|
-
JSON.stringify({
|
|
282
|
-
id: 'sa-pi-1',
|
|
283
|
-
rootSessionId: ROOT_SESSION,
|
|
284
|
-
agentName: 'pi-agent',
|
|
285
|
-
sessionFile: piSession,
|
|
286
|
-
slug: 'pi-sub',
|
|
287
|
-
status: 'completed',
|
|
288
|
-
}),
|
|
289
|
-
'utf8',
|
|
290
|
-
)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const signals = () => ({ agentDir, liveSessionDir: liveDir })
|
|
294
|
-
|
|
295
|
-
function logCallsWithReason(reason: string): number {
|
|
296
|
-
return appendEntries.mock.calls.filter(
|
|
297
|
-
([ct, d]) => ct === 'session-reader:log' && (d as { data?: { reason?: string } })?.data?.reason === reason,
|
|
298
|
-
).length
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ---------------------------------------------------------------------------
|
|
302
|
-
// 变体 M(manifest 在场,主路径)+ 场景 2 动作覆盖
|
|
303
|
-
// ---------------------------------------------------------------------------
|
|
304
|
-
|
|
305
|
-
describe('变体 M:manifest 主路径(instrument:manifest 锚与 entry 锚不同 session)', () => {
|
|
306
|
-
beforeEach(() => {
|
|
307
|
-
createZcodeDb(dbPath, '0.16.5', [sessARows(), sessBRows()])
|
|
308
|
-
// manifest 锚 → sess_bbb;liveSessionDir entry 锚 → sess_aaa:读到 ZBSENTINEL
|
|
309
|
-
// 而非 ZASENTINEL = 走了 manifest 主路径(entry 兜底未被进入的输出级证据)
|
|
310
|
-
writeZcodeManifest(SA_M)
|
|
311
|
-
writeLiveMainSession([recordLine('e1', SA_M, { sessionId: SESS_A, dbPath })])
|
|
312
|
-
writeRootSessionFile()
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
it('outline:读到 manifest 锚的 sess_bbb(ZBSENTINEL 在,ZASENTINEL 不在),两轮 T001/T002', async () => {
|
|
316
|
-
const r = await handleSessionRead({ action: 'outline', session: SA_M }, signals())
|
|
317
|
-
const text = r.content[0]?.text ?? ''
|
|
318
|
-
expect(text).toContain('ZBSENTINEL')
|
|
319
|
-
expect(text).not.toContain('ZASENTINEL')
|
|
320
|
-
// D9② 占位行折叠:T000 = converter 注入的 session_info 前置条目(纯记录,非对话),
|
|
321
|
-
// 折叠后不产行;对话轮保留原 turn.index(T001/T002),totalTurns 仍 = 2
|
|
322
|
-
expect(text).toContain('T001')
|
|
323
|
-
expect(text).toContain('T002')
|
|
324
|
-
expect(text).not.toMatch(/\bT000\b/)
|
|
325
|
-
// outline 的 isCompaction 恒 false 属已登记形态(§4 已接受代价)——无独立压缩 turn
|
|
326
|
-
expect(text).not.toContain('compaction')
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it('result:单 id 纯正文 = sess_bbb 最终 assistant 文本(末轮,非仅通知本轮)', async () => {
|
|
330
|
-
const r = await handleSessionRead({ action: 'result', session: SA_M }, signals())
|
|
331
|
-
expect(r.content[0]?.text).toBe('第二轮 ZBSENTINEL 收尾')
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
it('result 截断指针:zcode 路由抑制 read 半边(库路径二进制不可 read),只留 session_read detail 可执行半边', async () => {
|
|
335
|
-
const r = await handleSessionRead({ action: 'result', session: SA_M, limit: 5 }, signals())
|
|
336
|
-
const text = r.content[0]?.text ?? ''
|
|
337
|
-
expect(text).toContain('[truncated 5 of')
|
|
338
|
-
// 可执行半边在场(错误 → 权威源 → 重试闭环)
|
|
339
|
-
expect(text).toContain(`session_read { action:"detail", session:"${SA_M}" }`)
|
|
340
|
-
// read 半边被抑制:不指库路径,也不出现 "full text: read" 形态
|
|
341
|
-
expect(text).not.toContain(dbPath)
|
|
342
|
-
expect(text).not.toContain('full text: read')
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
it('detail:toolCall 全配对(bash)+ compaction custom entry 形态(渲染现状锚定)', async () => {
|
|
346
|
-
const r = await handleSessionRead({ action: 'detail', session: SA_M, turns: 'T000-T002' }, signals())
|
|
347
|
-
const text = r.content[0]?.text ?? ''
|
|
348
|
-
expect(text).toContain('ZZQFIXTURE')
|
|
349
|
-
expect(text).toContain('bash')
|
|
350
|
-
expect(text).toContain('[custom:zcode-import:compaction]')
|
|
351
|
-
// A6 断言强化①:compaction custom entry 计数等式(fixture 恰 1 个压缩点)——
|
|
352
|
-
// 防「既不多」方向无防护(多插压缩点/重复渲染的实现在此失败)
|
|
353
|
-
expect((text.match(/\[custom:zcode-import:compaction\]/g) ?? []).length).toBe(1)
|
|
354
|
-
// A6 断言强化②:toolCall/toolResult 按 toolCallId 全配对(全文态 entries 直读——
|
|
355
|
-
// 摘要态 ToolResultSummaryEntry 不携带 toolCallId,无法配对)。converter 契约
|
|
356
|
-
// 「未完成 tool 整对丢弃防 dangling」在此以集合配对相等 + 双向 0 dangling 锚定。
|
|
357
|
-
const full = await handleSessionRead(
|
|
358
|
-
{ action: 'detail', session: SA_M, turns: 'T000-T002', includeToolResult: true },
|
|
359
|
-
signals(),
|
|
360
|
-
)
|
|
361
|
-
const entries = (full.details as { entries: Array<Record<string, unknown>> }).entries
|
|
362
|
-
const callIds = new Set<string>()
|
|
363
|
-
const resultIds = new Set<string>()
|
|
364
|
-
for (const e of entries) {
|
|
365
|
-
const msg = e.message as { role?: unknown; content?: unknown; toolCallId?: unknown } | undefined
|
|
366
|
-
if (msg === undefined) continue
|
|
367
|
-
if (Array.isArray(msg.content)) {
|
|
368
|
-
for (const b of msg.content) {
|
|
369
|
-
if (b && typeof b === 'object' && (b as Record<string, unknown>).type === 'toolCall') {
|
|
370
|
-
const id = (b as Record<string, unknown>).id
|
|
371
|
-
if (typeof id === 'string') callIds.add(id)
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
if (msg.role === 'toolResult' && typeof msg.toolCallId === 'string') {
|
|
376
|
-
resultIds.add(msg.toolCallId)
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
expect(resultIds).toEqual(callIds) // 配对相等 = 无 toolCall 缺 result、无 result 悬空
|
|
380
|
-
expect(callIds.size).toBe(1) // fixture 恰 1 对(tc-1)——非平凡性锚(空集恒配对不构成证明)
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
it('search:ZZQFIXTURE 命中(canonical entries 走既有检索管线)', async () => {
|
|
384
|
-
const r = await handleSessionRead({ action: 'search', session: SA_M, pattern: 'ZZQFIXTURE' }, signals())
|
|
385
|
-
const text = r.content[0]?.text ?? ''
|
|
386
|
-
expect(text).toContain('ZZQFIXTURE')
|
|
387
|
-
expect(text).toContain('1 hit')
|
|
388
|
-
})
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
// ---------------------------------------------------------------------------
|
|
392
|
-
// 变体 E(manifest 缺位 → entry 兜底取末条)
|
|
393
|
-
// ---------------------------------------------------------------------------
|
|
394
|
-
|
|
395
|
-
describe('变体 E:manifest 缺位 → entry 兜底(liveSessionDir 内取文件顺序末条)', () => {
|
|
396
|
-
beforeEach(() => {
|
|
397
|
-
createZcodeDb(dbPath, '0.16.5', [sessARows(), sessBRows()])
|
|
398
|
-
// ≥2 条同 sa-id entry:首条 sess_aaa、末条 sess_bbb(F16 每轮覆写)——
|
|
399
|
-
// 取首条/合并的实现会输出 sess_aaa 行集,必失败
|
|
400
|
-
writeLiveMainSession([
|
|
401
|
-
recordLine('e1', SA_E, { sessionId: SESS_A, dbPath }),
|
|
402
|
-
recordLine('e2', SA_E, { sessionId: SESS_B, dbPath }),
|
|
403
|
-
])
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
it('result:兜底命中末条 → sess_bbb 正文(ZBSENTINEL 在,ZASENTINEL 不在)', async () => {
|
|
407
|
-
const r = await handleSessionRead({ action: 'result', session: SA_E }, signals())
|
|
408
|
-
expect(r.content[0]?.text).toBe('第二轮 ZBSENTINEL 收尾')
|
|
409
|
-
expect(r.content[0]?.text).not.toContain('ZASENTINEL')
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
it('兜底越界:liveSessionDir 内无该 sa-id 的任何 entry → zcode_record_not_found + 指引', async () => {
|
|
413
|
-
await expect(
|
|
414
|
-
handleSessionRead({ action: 'result', session: 'sa-nowhere-1' }, signals()),
|
|
415
|
-
).rejects.toThrow(/zcode_record_not_found[\s\S]*👉/)
|
|
416
|
-
})
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
// ---------------------------------------------------------------------------
|
|
420
|
-
// 场景 5:八类失败逐一断言(错误码 + 👉 指引 + ④ 顺序断言两种构造)
|
|
421
|
-
// ---------------------------------------------------------------------------
|
|
422
|
-
|
|
423
|
-
describe('场景 5 八类失败(§3.4 表逐行 + 检查顺序三段递进)', () => {
|
|
424
|
-
it('① manifest engine=zcode 但 engineHandle 整体缺席 → zcode_anchor_missing(导入对话框指引)', async () => {
|
|
425
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
426
|
-
writeZcodeManifest(SA_M, { omitEngineHandle: true })
|
|
427
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
428
|
-
/zcode_anchor_missing[\s\S]*导入/,
|
|
429
|
-
)
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
it('② entry 的 sessionRef 只留 sessionId、删 dbPath → zcode_anchor_missing + 结构化日志记「缺 dbPath」', async () => {
|
|
433
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
434
|
-
writeLiveMainSession([recordLine('e1', SA_M, { sessionId: SESS_B })])
|
|
435
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
436
|
-
'zcode_anchor_missing',
|
|
437
|
-
)
|
|
438
|
-
// 归因进结构化日志不进 LLM 可见部分(§3.4):缺失键名可区分(missing-dbPath)
|
|
439
|
-
expect(logCallsWithReason('missing-dbPath')).toBeGreaterThanOrEqual(1)
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
it('③ manifest 删除 + liveSessionDir 内无该 sa-id entry → zcode_record_not_found(三动作指引)', async () => {
|
|
443
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
444
|
-
writeLiveMainSession([recordLine('e1', 'sa-other-1', { sessionId: SESS_B, dbPath })])
|
|
445
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
446
|
-
/zcode_record_not_found[\s\S]*派发的那个会话[\s\S]*family[\s\S]*换一个已完成/,
|
|
447
|
-
)
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
it('③b pi 形态 entry(engine 缺省、sessionRef 无 dbPath)在场 + manifest 缺位 → zcode_record_not_found(engine 判别防误归因 anchor_missing)', async () => {
|
|
451
|
-
// 回归 A1:pi record register() 先于 manifest settle 的窗口——manifest 缺位 +
|
|
452
|
-
// entry 为 pi 形态。pi record 不是「旧版本 zcode 产物」(导入对话框指引不适用),
|
|
453
|
-
// 必须落 zcode_record_not_found;误归因 missing-dbPath → zcode_anchor_missing 即红。
|
|
454
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
455
|
-
writeLiveMainSession([piRecordLine('e-pi-1', SA_M)])
|
|
456
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
457
|
-
/zcode_record_not_found/,
|
|
458
|
-
)
|
|
459
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.not.toThrow(
|
|
460
|
-
/zcode_anchor_missing/,
|
|
461
|
-
)
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
it('④a 顺序断言(构造一):dbPath 集合外且文件真实存在 → zcode_db_path_forbidden', async () => {
|
|
465
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
466
|
-
const evilPath = join(root, 'evil-whitelist-probe.sqlite')
|
|
467
|
-
writeFileSync(evilPath, 'not a real db', 'utf8') // 存在性为真 → 走到路径段
|
|
468
|
-
writeZcodeManifest(SA_M, { sessionRef: { sessionId: SESS_B, dbPath: evilPath } })
|
|
469
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
470
|
-
/zcode_db_path_forbidden[\s\S]*👉/,
|
|
471
|
-
)
|
|
472
|
-
expect(existsSync(evilPath)).toBe(true)
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
it('④b 顺序断言(构造二):同一集合外路径但文件不存在 → zcode_db_unreadable(存在性先于路径)', async () => {
|
|
476
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
477
|
-
const missingEvil = join(root, 'evil-whitelist-probe-missing.sqlite')
|
|
478
|
-
writeZcodeManifest(SA_M, { sessionRef: { sessionId: SESS_B, dbPath: missingEvil } })
|
|
479
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
480
|
-
/zcode_db_unreadable[\s\S]*👉/,
|
|
481
|
-
)
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
it('⑤ 白名单形态路径但库文件缺席 → zcode_db_unreadable(含 -wal 告诫指引)', async () => {
|
|
485
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
486
|
-
const missingInLayout = join(root, ...ISOLATED_DB_SEGMENTS.slice(0, -1), 'missing.sqlite')
|
|
487
|
-
writeZcodeManifest(SA_M, { sessionRef: { sessionId: SESS_B, dbPath: missingInLayout } })
|
|
488
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
489
|
-
/zcode_db_unreadable[\s\S]*👉[\s\S]*-wal/,
|
|
490
|
-
)
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
it('⑥ schema_migration 版本超出已知集 → zcode_schema_drift(升级 taiji 指引)', async () => {
|
|
494
|
-
createZcodeDb(dbPath, '9.9.9', [sessBRows()])
|
|
495
|
-
writeZcodeManifest(SA_M)
|
|
496
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
497
|
-
/zcode_schema_drift[\s\S]*9\.9\.9[\s\S]*👉/,
|
|
498
|
-
)
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
it('⑦ 传入 sess_ 形态 id → zcode_param_invalid(换完整 sa- id 指引)', async () => {
|
|
502
|
-
await expect(
|
|
503
|
-
handleSessionRead({ action: 'result', session: SESS_B }, signals()),
|
|
504
|
-
).rejects.toThrow(/zcode_param_invalid[\s\S]*👉[\s\S]*sa-/)
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
it('⑧ 锚可解析 + 库可开 + schema 兼容,但库内 session 行已删 → zcode_session_not_found', async () => {
|
|
508
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
509
|
-
writeZcodeManifest(SA_M)
|
|
510
|
-
// 模拟 zcode 侧 GC:删 sess_bbb 的全部行(db 文件与 schema 保留)
|
|
511
|
-
const db = new DatabaseSync(dbPath)
|
|
512
|
-
try {
|
|
513
|
-
db.exec("DELETE FROM part WHERE session_id = 'sess_bbb'")
|
|
514
|
-
db.exec("DELETE FROM message WHERE session_id = 'sess_bbb'")
|
|
515
|
-
db.exec("DELETE FROM session WHERE id = 'sess_bbb'")
|
|
516
|
-
} finally {
|
|
517
|
-
db.close()
|
|
518
|
-
}
|
|
519
|
-
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
520
|
-
/zcode_session_not_found[\s\S]*👉[\s\S]*原 sa-id[\s\S]*family/,
|
|
521
|
-
)
|
|
522
|
-
})
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
// ---------------------------------------------------------------------------
|
|
526
|
-
// 驱动探测失败的错误契约(跨包漂移检查 + L4 包装形态映射)
|
|
527
|
-
// ---------------------------------------------------------------------------
|
|
528
|
-
|
|
529
|
-
describe('驱动探测失败错误范围:跨包契约 + L4 包装形态映射', () => {
|
|
530
|
-
it('跨包契约:SQLITE_DRIVER_UNSUPPORTED_MARK 子串在 zcode-session-source sqlite-driver 源文内(漂移即红)', () => {
|
|
531
|
-
// 驱动探测错误无专用错误类型(该包不导出子类),reader 按消息特征识别——mark 与
|
|
532
|
-
// sqlite-driver 源内错误字面量的绑定由本测试机器锚定(entry-anchor.test.ts 协议
|
|
533
|
-
// 字面量跨包漂移检查同范式):源文措辞变更不再含 mark → host_unsupported 契约死亡,红。
|
|
534
|
-
const source = readFileSync(
|
|
535
|
-
new URL('../../../../../packages/zcode-session-source/src/sqlite-driver.ts', import.meta.url),
|
|
536
|
-
'utf8',
|
|
537
|
-
)
|
|
538
|
-
expect(source).toContain(SQLITE_DRIVER_UNSUPPORTED_MARK)
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
it('L4 包装形态映射:SqliteUnreadableError(message 含 mark,last failure 并入)→ host_unsupported(R1 回归锚)', () => {
|
|
542
|
-
// 生产路径上驱动探测错误恒经 recovery L4 包装(last failure 以字符串并入
|
|
543
|
-
// SqliteUnreadableError.message,mark 随之存活)——直接构造该形态,锁定
|
|
544
|
-
// instanceof 分支内的二次判别(分支顺序错误 → 误落 db_unreadable,本用例红)。
|
|
545
|
-
const wrapped = new SqliteUnreadableError(
|
|
546
|
-
['L1-direct', 'L2-immutable'],
|
|
547
|
-
'zcode session db unreadable (recovery ladder exhausted): /tmp/none.sqlite — ' +
|
|
548
|
-
`last failure: 当前 node 运行时${SQLITE_DRIVER_UNSUPPORTED_MARK}(需 >=22.13),且宿主非 bun 运行时`,
|
|
549
|
-
)
|
|
550
|
-
const msg = zcodeReadErrorMessage(wrapped, agentDir)
|
|
551
|
-
expect(msg).toContain('zcode_host_unsupported')
|
|
552
|
-
expect(msg).not.toContain('zcode_db_unreadable')
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
it('无 mark 的 SqliteUnreadableError 维持 db_unreadable 面(对照:attempted 链进 detail)', () => {
|
|
556
|
-
const plain = new SqliteUnreadableError(
|
|
557
|
-
['L1-direct'],
|
|
558
|
-
'zcode session db unreadable (recovery ladder exhausted): /tmp/none.sqlite — last failure: file is not a database',
|
|
559
|
-
)
|
|
560
|
-
const msg = zcodeReadErrorMessage(plain, agentDir)
|
|
561
|
-
expect(msg).toContain('zcode_db_unreadable')
|
|
562
|
-
expect(msg).toContain('L1-direct')
|
|
563
|
-
expect(msg).not.toContain('zcode_host_unsupported')
|
|
564
|
-
})
|
|
565
|
-
|
|
566
|
-
it('跨包契约:ZCODE_DB_MISSING_MARK 子串在 zcode-session-source sqlite-access 源文内(漂移即红)', () => {
|
|
567
|
-
// 库文件缺失(openZcodeSessionDb 开头 existsSync 失败)无专用错误类型(抛普通
|
|
568
|
-
// Error),reader 按消息特征识别(SQLITE_DRIVER_UNSUPPORTED_MARK 契约测试同范式):
|
|
569
|
-
// sqlite-access 源文措辞变更不再含 mark → db_unreadable 面死亡、TOCTOU 缺失误标
|
|
570
|
-
// schema_drift,红。
|
|
571
|
-
const source = readFileSync(
|
|
572
|
-
new URL('../../../../../packages/zcode-session-source/src/sqlite-access.ts', import.meta.url),
|
|
573
|
-
'utf8',
|
|
574
|
-
)
|
|
575
|
-
expect(source).toContain(ZCODE_DB_MISSING_MARK)
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
it('TOCTOU 窗口:开库期库文件缺失普通 Error → db_unreadable 面(不误标 schema_drift,§3.4 第 4 行)', () => {
|
|
579
|
-
// 白名单闸 existsSync 通过后、openZcodeSessionDb 开库前文件被删的窗口:sqlite-access
|
|
580
|
-
// 抛普通 Error「db 文件不存在:<dbPath>」——落 db_unreadable(环境恢复指引),编程
|
|
581
|
-
// 错误兜底(schema_drift)只接真正的查询期 schema 漂移域。
|
|
582
|
-
const msg = zcodeReadErrorMessage(new Error(`db 文件不存在:/tmp/fixture/db.sqlite`), agentDir)
|
|
583
|
-
expect(msg).toContain('zcode_db_unreadable')
|
|
584
|
-
expect(msg).toContain('db 文件不存在:/tmp/fixture/db.sqlite')
|
|
585
|
-
expect(msg).toContain('👉')
|
|
586
|
-
expect(msg).not.toContain('zcode_schema_drift')
|
|
587
|
-
})
|
|
588
|
-
|
|
589
|
-
it('对照:不含缺失 mark 的普通 Error 仍落 schema_drift 兜底(编程错误域语义不扩)', () => {
|
|
590
|
-
const msg = zcodeReadErrorMessage(new Error('TypeError: cannot read properties of undefined'), agentDir)
|
|
591
|
-
expect(msg).toContain('zcode_schema_drift')
|
|
592
|
-
expect(msg).not.toContain('zcode_db_unreadable')
|
|
593
|
-
})
|
|
594
|
-
})
|
|
595
|
-
|
|
596
|
-
// ---------------------------------------------------------------------------
|
|
597
|
-
// family 接线(buildFamilyFromFs zcode 分支 + sa-id family 路由)
|
|
598
|
-
// ---------------------------------------------------------------------------
|
|
599
|
-
|
|
600
|
-
describe('family:zcode 节点接线(D5-1 两态)与 sa-id family 路由', () => {
|
|
601
|
-
beforeEach(() => {
|
|
602
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
603
|
-
writeZcodeManifest(SA_M)
|
|
604
|
-
writeRootSessionFile()
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
it('sa-id family:zcode 路由 → rootSessionId 视图,subagents 含 zcode 节点且 cleanedUp=false', async () => {
|
|
608
|
-
const r = await handleSessionRead({ action: 'family', session: SA_M }, signals())
|
|
609
|
-
const text = r.content[0]?.text ?? ''
|
|
610
|
-
expect(text).toContain(SA_M)
|
|
611
|
-
const family = r.details as { subagents: Array<{ sessionId: string; cleanedUp?: boolean }> }
|
|
612
|
-
const node = family.subagents.find((s) => s.sessionId === SA_M)
|
|
613
|
-
expect(node?.cleanedUp).toBe(false) // 库可达态(D5-1:锚可解析 ∧ 库文件存在)
|
|
614
|
-
})
|
|
615
|
-
|
|
616
|
-
it('sa-id family recursive=true:rootSessionId 反查回填 fileName → 树 main root 填真实 session 路径(MF-1 链路)', async () => {
|
|
617
|
-
const r = await handleSessionRead({ action: 'family', session: SA_M, recursive: true }, signals())
|
|
618
|
-
const tree = (
|
|
619
|
-
r.details as { tree: { root: { type: string; sessionId: string; sessionFile?: string } } }
|
|
620
|
-
).tree
|
|
621
|
-
expect(tree.root.type).toBe('main')
|
|
622
|
-
expect(tree.root.sessionId).toBe(ROOT_SESSION)
|
|
623
|
-
// 反查命中默认根的 root session 文件(writeRootSessionFile 落的路径),非空串占位
|
|
624
|
-
expect(tree.root.sessionFile).toBe(join(agentDir, 'sessions', `1700000000000_${ROOT_SESSION}.jsonl`))
|
|
625
|
-
})
|
|
626
|
-
|
|
627
|
-
it('rootSessionId family:buildFamilyFromFs 接线后 zcode 节点进列表(真实 family action 输出)', async () => {
|
|
628
|
-
const r = await handleSessionRead({ action: 'family', session: ROOT_SESSION }, signals())
|
|
629
|
-
const family = r.details as { subagents: Array<{ sessionId: string; cleanedUp?: boolean }> }
|
|
630
|
-
expect(family.subagents.map((s) => s.sessionId)).toContain(SA_M)
|
|
631
|
-
})
|
|
632
|
-
|
|
633
|
-
it('库不存在态:dbPath 改指白名单布局内缺席路径 → cleanedUp=true(GC 语义,不报错)', async () => {
|
|
634
|
-
writeZcodeManifest(SA_M, {
|
|
635
|
-
sessionRef: { sessionId: SESS_B, dbPath: join(root, ...ISOLATED_DB_SEGMENTS.slice(0, -1), 'gone.sqlite') },
|
|
636
|
-
})
|
|
637
|
-
const r = await handleSessionRead({ action: 'family', session: ROOT_SESSION }, signals())
|
|
638
|
-
const family = r.details as { subagents: Array<{ sessionId: string; cleanedUp?: boolean }> }
|
|
639
|
-
expect(family.subagents.find((s) => s.sessionId === SA_M)?.cleanedUp).toBe(true)
|
|
640
|
-
})
|
|
641
|
-
|
|
642
|
-
it('反向断言:库文件在但库内 session 行被删 → family 仍 cleanedUp=false(列表视图不开库的分工)', async () => {
|
|
643
|
-
const db = new DatabaseSync(dbPath)
|
|
644
|
-
try {
|
|
645
|
-
db.exec("DELETE FROM part WHERE session_id = 'sess_bbb'")
|
|
646
|
-
db.exec("DELETE FROM message WHERE session_id = 'sess_bbb'")
|
|
647
|
-
db.exec("DELETE FROM session WHERE id = 'sess_bbb'")
|
|
648
|
-
} finally {
|
|
649
|
-
db.close()
|
|
650
|
-
}
|
|
651
|
-
const r = await handleSessionRead({ action: 'family', session: ROOT_SESSION }, signals())
|
|
652
|
-
const family = r.details as { subagents: Array<{ sessionId: string; cleanedUp?: boolean }> }
|
|
653
|
-
expect(family.subagents.find((s) => s.sessionId === SA_M)?.cleanedUp).toBe(false)
|
|
654
|
-
})
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
// ---------------------------------------------------------------------------
|
|
658
|
-
// pi 穿插不受影响(场景 3 抽样:zcode 失败调用穿插前后 pi 输出不变)
|
|
659
|
-
// ---------------------------------------------------------------------------
|
|
660
|
-
|
|
661
|
-
describe('pi 穿插不受影响:同 fixture 内 pi 会话读取与 zcode 调用互不干扰', () => {
|
|
662
|
-
beforeEach(() => {
|
|
663
|
-
createZcodeDb(dbPath, '0.16.5', [sessBRows()])
|
|
664
|
-
writeZcodeManifest(SA_M)
|
|
665
|
-
writePiFixture()
|
|
666
|
-
})
|
|
667
|
-
|
|
668
|
-
it('result:pi sa-id 正文穿插 zcode 失败调用前后一致(PI-SENTINEL 恒在)', async () => {
|
|
669
|
-
// zcode 调用先失败(锚缺位形态)
|
|
670
|
-
writeZcodeManifest('sa-broken-1', { omitEngineHandle: true })
|
|
671
|
-
await expect(handleSessionRead({ action: 'result', session: 'sa-broken-1' }, signals())).rejects.toThrow(
|
|
672
|
-
'zcode_anchor_missing',
|
|
673
|
-
)
|
|
674
|
-
const pi = await handleSessionRead({ action: 'result', session: 'sa-pi-1' }, signals())
|
|
675
|
-
expect(pi.content[0]?.text).toContain('PI-SENTINEL 正文内容')
|
|
676
|
-
// 再穿插一次 zcode 成功调用,pi 输出仍一致
|
|
677
|
-
const piAgain = await handleSessionRead({ action: 'result', session: 'sa-pi-1' }, signals())
|
|
678
|
-
expect(piAgain.content[0]?.text).toBe(pi.content[0]?.text)
|
|
679
|
-
})
|
|
680
|
-
|
|
681
|
-
it('uuid 形态(resolveSessionId 形态③)与 pi sa-id 现路径零变化', async () => {
|
|
682
|
-
const pi = await handleSessionRead({ action: 'result', session: 'sa-pi-1' }, { agentDir })
|
|
683
|
-
expect(pi.content[0]?.text).toContain('PI-SENTINEL')
|
|
684
|
-
})
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
// ---------------------------------------------------------------------------
|
|
688
|
-
// D4 schema 零变化(快照逐字节对照 + 合法形态冒烟)
|
|
689
|
-
// ---------------------------------------------------------------------------
|
|
690
|
-
|
|
691
|
-
/** `session_read` 输入 schema 的 JSON 序列化快照(防无意漂移的逐字节锚)。
|
|
692
|
-
*
|
|
693
|
-
* 快照锚的意图 = 无意漂移拦截(参数增删 / 文案改动即红,迫使有意确认);
|
|
694
|
-
* 有意的声明侧压缩须同批重新生成快照。当前快照 = tool-declaration-slimming
|
|
695
|
-
* U-A1 压缩终态(D1 三层分工:action 功能语义唯一承载点 = 枚举 description;
|
|
696
|
-
* 工具 description 只留定位 + 边界;guidelines 只留纪律)。重新生成:
|
|
697
|
-
* tsx 起 extension 捕获 registerTool 的 parameters 后 JSON.stringify。
|
|
698
|
-
*/
|
|
699
|
-
const SCHEMA_SNAPSHOT =
|
|
700
|
-
'{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["find","famil'
|
|
701
|
-
+ 'y","outline","expand","detail","search","export","extract","workflow","result","doctor"],"descriptio'
|
|
702
|
-
+ 'n":"find (locate session), family (relations; recursive=tree), outline (turn overview), expand (one '
|
|
703
|
-
+ 'turn), detail (full text), search (grep, cross-session, ≤10 ids), export (to file), extract (message'
|
|
704
|
-
+ 's/commands/files/commits/tool results), workflow (run overview), result (subagent final text, batch '
|
|
705
|
-
+ '≤10), doctor (environment diagnostics)."},"session":{"type":"string","description":"Session id, uuid'
|
|
706
|
-
+ ' fragment (e6c96), subagent id (sa-xxx), or .jsonl path. Required by every action except find/doctor'
|
|
707
|
-
+ '; result/search accept ≤10 comma-separated ids. # prefix auto-stripped."},"query":{"type":"string","'
|
|
708
|
-
+ 'description":"find: uuid fragment / filename / name keyword / \\"recent\\" (recent N)."},"turns":{"typ'
|
|
709
|
-
+ 'e":"string","description":"detail/extract: turn range \\"T013-T015\\" or \\"T013\\"."},"turn":{"type":"s'
|
|
710
|
-
+ 'tring","description":"expand: turn \\"T013\\"."},"pattern":{"type":"string","description":"search: sub'
|
|
711
|
-
+ 'string or regex."},"scope":{"type":"string","enum":["all","user","assistant","toolResult"],"descript'
|
|
712
|
-
+ 'ion":"search: filter scope. Default all."},"format":{"type":"string","enum":["outline","full","famil'
|
|
713
|
-
+ 'y"],"description":"export: materialized form. Default outline."},"includeToolResult":{"type":"boolea'
|
|
714
|
-
+ 'n","description":"detail/export: include toolResult text. Default false."},"includeThinking":{"type"'
|
|
715
|
-
+ ':"boolean","description":"detail: include thinking blocks. Default false."},"allBranches":{"type":"b'
|
|
716
|
-
+ 'oolean","description":"outline/export: include abandoned side-branches. Default false."},"granularit'
|
|
717
|
-
+ 'y":{"type":"string","enum":["turn","entry"],"description":"outline: turn-level or entry-flat. Defaul'
|
|
718
|
-
+ 't turn."},"cwd":{"type":"string","description":"find: filter by cwd."},"source":{"type":"string","en'
|
|
719
|
-
+ 'um":["main","subagent"],"description":"\\"main\\" = sessions/, \\"subagent\\" = subagents/. Default both'
|
|
720
|
-
+ '."},"limit":{"type":"number","minimum":1,"description":"find/search: max results (default 20). resul'
|
|
721
|
-
+ 't: max chars per item (default 8000, truncated with pointer)."},"what":{"type":"string","enum":["use'
|
|
722
|
-
+ 'r-messages","commands","files","commits","tool-results"],"description":"extract: what to pull (requi'
|
|
723
|
-
+ 'red)."},"tool":{"type":"string","description":"extract: filter by tool name (e.g. \\"bash\\")."},"runI'
|
|
724
|
-
+ 'd":{"type":"string","description":"workflow: focus one run (omit = all)."},"recursive":{"type":"bool'
|
|
725
|
-
+ 'ean","description":"family: nested execution tree. Default false (flat)."},"includeSubagents":{"type'
|
|
726
|
-
+ '":"boolean","description":"doctor: also scan the subagent session root. Default false."}}}';
|
|
727
|
-
|
|
728
|
-
describe('session_read schema 快照(D4:不加任何工具参数,逐字节一致)', () => {
|
|
729
|
-
it('TypeBox schema JSON 序列化与基线快照逐字节一致', () => {
|
|
730
|
-
const registerTool = vi.fn()
|
|
731
|
-
const pi = { registerTool, registerCommand: vi.fn(), addAutocompleteProvider: vi.fn(), on: vi.fn() }
|
|
732
|
-
;(sessionReaderExtension as unknown as (p: unknown) => void)(pi)
|
|
733
|
-
const toolDef = registerTool.mock.calls[0][0] as { parameters: unknown }
|
|
734
|
-
expect(JSON.stringify(toolDef.parameters)).toBe(SCHEMA_SNAPSHOT)
|
|
735
|
-
})
|
|
736
|
-
|
|
737
|
-
it('既有合法调用形态仍通过 schema 校验(无新增必填参数)', () => {
|
|
738
|
-
const registerTool = vi.fn()
|
|
739
|
-
const pi = { registerTool, registerCommand: vi.fn(), addAutocompleteProvider: vi.fn(), on: vi.fn() }
|
|
740
|
-
;(sessionReaderExtension as unknown as (p: unknown) => void)(pi)
|
|
741
|
-
const schema = (registerTool.mock.calls[0][0] as { parameters: unknown }).parameters
|
|
742
|
-
expect(Check(schema, { action: 'result', session: 'sa-xxx' })).toBe(true)
|
|
743
|
-
expect(Check(schema, { action: 'outline', session: 'e6c96' })).toBe(true)
|
|
744
|
-
})
|
|
745
|
-
})
|
|
746
|
-
|
|
747
|
-
// ---------------------------------------------------------------------------
|
|
748
|
-
// D2 锚词保留清单(index.ts guidelines 上方注释登记的清单在此逐词 grep 锚定)
|
|
749
|
-
// ---------------------------------------------------------------------------
|
|
750
|
-
|
|
751
|
-
describe('session_read 锚词保留清单(D2:guidelines 纪律逐词锚定)', () => {
|
|
752
|
-
// SCHEMA_SNAPSHOT 只逐字节锚 parameters schema,不覆盖 guidelines——本块是
|
|
753
|
-
// guidelines 锚词的机器防线:弱模型回归(P7/PB-weak)后对本工具声明侧的
|
|
754
|
-
// 再压缩 / 措辞改动,逐词红灯迫使有意确认。清单与 index.ts 注释登记一致:
|
|
755
|
-
// 1. "Progressive reading" —— 阅读纪律 outline → expand → detail
|
|
756
|
-
// 2. "Never read raw .jsonl" —— 禁直读原始 session 文件
|
|
757
|
-
// 3. "sa- ids route" —— zcode 路由对 LLM 的唯一可见部分(U9)
|
|
758
|
-
// 4. "👉 recovery hint" —— 错误范围恢复指针契约
|
|
759
|
-
// 5. "doctor" —— 环境自检入口(另被上方快照 action 枚举间接锚定)
|
|
760
|
-
it('guidelines 5 锚词逐个在位(缺一词即红)', () => {
|
|
761
|
-
const registerTool = vi.fn()
|
|
762
|
-
const pi = { registerTool, registerCommand: vi.fn(), addAutocompleteProvider: vi.fn(), on: vi.fn() }
|
|
763
|
-
;(sessionReaderExtension as unknown as (p: unknown) => void)(pi)
|
|
764
|
-
const toolDef = registerTool.mock.calls[0][0] as { promptGuidelines: string[] }
|
|
765
|
-
const guidelines = toolDef.promptGuidelines.join('\n')
|
|
766
|
-
expect(guidelines).toContain('Progressive reading')
|
|
767
|
-
expect(guidelines).toContain('Never read raw .jsonl')
|
|
768
|
-
expect(guidelines).toContain('sa- ids route')
|
|
769
|
-
expect(guidelines).toContain('👉 recovery hint')
|
|
770
|
-
expect(guidelines).toContain('doctor')
|
|
771
|
-
})
|
|
772
|
-
})
|