@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,382 +1,762 @@
|
|
|
1
|
+
// entry-anchor.test.ts —— session-reader C04 簇合并产物(round5-G)。
|
|
2
|
+
// 共同被测业务模块:`src/discovery/entry-anchor.ts`。mock 相容:全簇零 vi.mock。
|
|
3
|
+
// 顶层正文按来源包 describe 纯前缀分节,it 层名零改动。
|
|
4
|
+
//
|
|
5
|
+
// 来源:
|
|
6
|
+
// - src/__tests__/entry-anchor.test.ts(18 用例,skip 0)
|
|
7
|
+
// - src/__tests__/zcode-manifest.test.ts(18 用例,skip 0)
|
|
8
|
+
|
|
1
9
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
10
|
import { tmpdir } from 'node:os'
|
|
3
11
|
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
|
|
12
|
+
import { join, dirname } from 'node:path'
|
|
6
13
|
import { SUBAGENT_RECORD_CUSTOM_TYPE, SUBAGENT_RECORD_ENTRY_VERSION } from '@zhushanwen/subagent-core'
|
|
7
|
-
|
|
8
14
|
import { findZcodeEntryAnchor, type ZcodeAnchor } from '../discovery/entry-anchor.js'
|
|
15
|
+
import { readZcodeManifest, listZcodeManifests } from '../discovery/zcode-manifest.js'
|
|
9
16
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
/** subagent-record custom entry 行(顶层 id = entry id;data.id = record id 即 sa-id)。 */
|
|
31
|
-
function recordLine(
|
|
32
|
-
entryId: string,
|
|
33
|
-
saId: string,
|
|
34
|
-
sessionRef: Record<string, string>,
|
|
35
|
-
v = 2,
|
|
36
|
-
): string {
|
|
37
|
-
// 登记 §3.3:v1 全量快照形态已删——锚源 = v2 终态条(settled 携带 engine/engineHandle)。
|
|
38
|
-
return JSON.stringify({
|
|
39
|
-
type: 'custom',
|
|
40
|
-
customType: 'subagent-record',
|
|
41
|
-
id: entryId,
|
|
42
|
-
parentId: null,
|
|
43
|
-
data: {
|
|
44
|
-
v,
|
|
45
|
-
kind: 'settled',
|
|
46
|
-
id: saId,
|
|
47
|
-
status: 'idle',
|
|
48
|
-
stopReason: 'completed',
|
|
49
|
-
endedAt: 2,
|
|
50
|
-
turns: 1,
|
|
51
|
-
totalTokens: 1,
|
|
52
|
-
engine: 'zcode',
|
|
53
|
-
engineHandle: { sessionRef, poolKey: 'shared' },
|
|
54
|
-
},
|
|
17
|
+
// ======== 来源:entry-anchor.test.ts ========
|
|
18
|
+
describe("entry-anchor", () => {
|
|
19
|
+
// ============================================================
|
|
20
|
+
// 变体 E(设计 §4 场景 1a):manifest 缺席时 entry 兜底定位(F16 每轮覆写 → 取文件
|
|
21
|
+
// 顺序末条,collectLastRecordEntries 同构)。
|
|
22
|
+
//
|
|
23
|
+
// 单元边界:本文件只测「定位」(sa-id → sessionRef 锚);「读到 sess-B 行集内容」的
|
|
24
|
+
// 库读取断言属 U9 集成测试(场景 1a 变体 E 全量断言),此处不引入 sqlite 依赖。
|
|
25
|
+
// sess-A / sess-B 本身即两个可区分的锚值——取首条或合并的实现会直接失败。
|
|
26
|
+
// ============================================================
|
|
27
|
+
|
|
28
|
+
const DB_PATH = '/data/agent/engines/zcode/session-db/db.sqlite'
|
|
29
|
+
|
|
30
|
+
let dir: string
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
dir = mkdtempSync(join(tmpdir(), 'entry-anchor-test-'))
|
|
55
34
|
})
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
function messageLine(entryId: string, text: string): string {
|
|
60
|
-
return JSON.stringify({
|
|
61
|
-
type: 'message',
|
|
62
|
-
id: entryId,
|
|
63
|
-
parentId: null,
|
|
64
|
-
message: { role: 'user', content: text },
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
65
38
|
})
|
|
66
|
-
}
|
|
67
39
|
|
|
68
|
-
/**
|
|
40
|
+
/** subagent-record custom entry 行(顶层 id = entry id;data.id = record id 即 sa-id)。 */
|
|
41
|
+
function recordLine(
|
|
42
|
+
entryId: string,
|
|
43
|
+
saId: string,
|
|
44
|
+
sessionRef: Record<string, string>,
|
|
45
|
+
v = 2,
|
|
46
|
+
): string {
|
|
47
|
+
// 登记 §3.3:v1 全量快照形态已删——锚源 = v2 终态条(settled 携带 engine/engineHandle)。
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
type: 'custom',
|
|
50
|
+
customType: 'subagent-record',
|
|
51
|
+
id: entryId,
|
|
52
|
+
parentId: null,
|
|
53
|
+
data: {
|
|
54
|
+
v,
|
|
55
|
+
kind: 'settled',
|
|
56
|
+
id: saId,
|
|
57
|
+
status: 'idle',
|
|
58
|
+
stopReason: 'completed',
|
|
59
|
+
endedAt: 2,
|
|
60
|
+
turns: 1,
|
|
61
|
+
totalTokens: 1,
|
|
62
|
+
engine: 'zcode',
|
|
63
|
+
engineHandle: { sessionRef, poolKey: 'shared' },
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** pi 形态 message 行(真实感填充)。 */
|
|
69
|
+
function messageLine(entryId: string, text: string): string {
|
|
70
|
+
return JSON.stringify({
|
|
71
|
+
type: 'message',
|
|
72
|
+
id: entryId,
|
|
73
|
+
parentId: null,
|
|
74
|
+
message: { role: 'user', content: text },
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
69
79
|
* v2 终态条行(W1 D1:`{v:2, kind:"settled", id, engine?, engineHandle?, ...}`——
|
|
70
80
|
* 载荷形态对齐 subagent-core SubagentRecordSettledEntryData,锚取 engineHandle.sessionRef)。
|
|
71
81
|
*/
|
|
72
|
-
function v2SettledLine(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
): string {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
82
|
+
function v2SettledLine(
|
|
83
|
+
entryId: string,
|
|
84
|
+
saId: string,
|
|
85
|
+
opts: {
|
|
86
|
+
sessionRef?: Record<string, string>
|
|
87
|
+
engine?: string
|
|
88
|
+
} = {},
|
|
89
|
+
): string {
|
|
90
|
+
const data: Record<string, unknown> = {
|
|
91
|
+
v: 2,
|
|
92
|
+
kind: 'settled',
|
|
93
|
+
id: saId,
|
|
94
|
+
status: 'idle',
|
|
95
|
+
stopReason: 'end_turn',
|
|
96
|
+
endedAt: 123,
|
|
97
|
+
turns: 1,
|
|
98
|
+
totalTokens: 10,
|
|
99
|
+
model: undefined,
|
|
100
|
+
thinkingLevel: undefined,
|
|
101
|
+
}
|
|
102
|
+
if (opts.engine !== undefined) data.engine = opts.engine
|
|
103
|
+
if (opts.sessionRef !== undefined) {
|
|
104
|
+
data.engineHandle = { sessionRef: opts.sessionRef, poolKey: 'shared' }
|
|
105
|
+
}
|
|
106
|
+
return JSON.stringify({
|
|
107
|
+
type: 'custom',
|
|
108
|
+
customType: 'subagent-record',
|
|
109
|
+
id: entryId,
|
|
110
|
+
parentId: null,
|
|
111
|
+
data,
|
|
112
|
+
})
|
|
95
113
|
}
|
|
96
|
-
return JSON.stringify({
|
|
97
|
-
type: 'custom',
|
|
98
|
-
customType: 'subagent-record',
|
|
99
|
-
id: entryId,
|
|
100
|
-
parentId: null,
|
|
101
|
-
data,
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
114
|
|
|
105
|
-
/**
|
|
115
|
+
/**
|
|
106
116
|
* v2 注册条行(`{v:2, kind:"registered", id, agent, task, ...}`——身份域字段,不携带
|
|
107
117
|
* engineHandle;设计 D1 裁决「sessionRef 双键取自终态条」,注册条目不是锚源)。
|
|
108
118
|
*/
|
|
109
|
-
function v2RegisteredLine(entryId: string, saId: string): string {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** 缺 dbPath 的 sessionRef(pi record 形态:只有 sessionId 单键)。 */
|
|
131
|
-
const REF_NO_DBPATH = { sessionId: 'sess-pi-x' }
|
|
132
|
-
|
|
133
|
-
/** 缺 sessionId 的 sessionRef(半键)。 */
|
|
134
|
-
const REF_NO_SESSION_ID = { dbPath: DB_PATH }
|
|
135
|
-
|
|
136
|
-
function writeMainSession(name: string, lines: string[]): string {
|
|
137
|
-
mkdirSync(dir, { recursive: true })
|
|
138
|
-
const file = join(dir, name)
|
|
139
|
-
writeFileSync(file, lines.join('\n') + '\n', 'utf8')
|
|
140
|
-
return file
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
describe('变体 E:同 sa-id 多条 entry 取文件顺序末条(F16 覆写)', () => {
|
|
144
|
-
it('首条 sess-A / 末条 sess-B → 命中 sess-B 且 ≠ sess-A(取首条/合并必失败)', async () => {
|
|
145
|
-
const file = writeMainSession('main.jsonl', [
|
|
146
|
-
JSON.stringify({ type: 'session', id: 'main-1', cwd: '/tmp/proj' }),
|
|
147
|
-
messageLine('m1', 'dispatch subagent'),
|
|
148
|
-
recordLine('e1', 'sa-1', { sessionId: 'sess-A', dbPath: DB_PATH }),
|
|
149
|
-
messageLine('m2', 'round 1 done'),
|
|
150
|
-
recordLine('e2', 'sa-1', { sessionId: 'sess-B', dbPath: DB_PATH }),
|
|
151
|
-
])
|
|
152
|
-
|
|
153
|
-
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
154
|
-
|
|
155
|
-
// 非平凡断言:两个不同锚值使「末条 vs 首条」结果可区分
|
|
156
|
-
expect(anchor).toBeDefined()
|
|
157
|
-
expect(anchor?.sessionId).toBe('sess-B')
|
|
158
|
-
expect(anchor?.sessionId).not.toBe('sess-A')
|
|
159
|
-
expect(anchor?.dbPath).toBe(DB_PATH)
|
|
160
|
-
})
|
|
119
|
+
function v2RegisteredLine(entryId: string, saId: string): string {
|
|
120
|
+
return JSON.stringify({
|
|
121
|
+
type: 'custom',
|
|
122
|
+
customType: 'subagent-record',
|
|
123
|
+
id: entryId,
|
|
124
|
+
parentId: null,
|
|
125
|
+
data: {
|
|
126
|
+
v: 2,
|
|
127
|
+
kind: 'registered',
|
|
128
|
+
id: saId,
|
|
129
|
+
agent: 'dev',
|
|
130
|
+
task: 't',
|
|
131
|
+
slug: 's',
|
|
132
|
+
origin: 'tool',
|
|
133
|
+
rootSessionId: 'root-1',
|
|
134
|
+
depth: 0,
|
|
135
|
+
startedAt: 1,
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
}
|
|
161
139
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
140
|
+
/** 缺 dbPath 的 sessionRef(pi record 形态:只有 sessionId 单键)。 */
|
|
141
|
+
const REF_NO_DBPATH = { sessionId: 'sess-pi-x' }
|
|
142
|
+
|
|
143
|
+
/** 缺 sessionId 的 sessionRef(半键)。 */
|
|
144
|
+
const REF_NO_SESSION_ID = { dbPath: DB_PATH }
|
|
145
|
+
|
|
146
|
+
function writeMainSession(name: string, lines: string[]): string {
|
|
147
|
+
mkdirSync(dir, { recursive: true })
|
|
148
|
+
const file = join(dir, name)
|
|
149
|
+
writeFileSync(file, lines.join('\n') + '\n', 'utf8')
|
|
150
|
+
return file
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
describe('变体 E:同 sa-id 多条 entry 取文件顺序末条(F16 覆写)', () => {
|
|
154
|
+
it('首条 sess-A / 末条 sess-B → 命中 sess-B 且 ≠ sess-A(取首条/合并必失败)', async () => {
|
|
155
|
+
const file = writeMainSession('main.jsonl', [
|
|
156
|
+
JSON.stringify({ type: 'session', id: 'main-1', cwd: '/tmp/proj' }),
|
|
157
|
+
messageLine('m1', 'dispatch subagent'),
|
|
158
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-A', dbPath: DB_PATH }),
|
|
159
|
+
messageLine('m2', 'round 1 done'),
|
|
160
|
+
recordLine('e2', 'sa-1', { sessionId: 'sess-B', dbPath: DB_PATH }),
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
164
|
+
|
|
165
|
+
// 非平凡断言:两个不同锚值使「末条 vs 首条」结果可区分
|
|
166
|
+
expect(anchor).toBeDefined()
|
|
167
|
+
expect(anchor?.sessionId).toBe('sess-B')
|
|
168
|
+
expect(anchor?.sessionId).not.toBe('sess-A')
|
|
169
|
+
expect(anchor?.dbPath).toBe(DB_PATH)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('多条不同 sa-id 交错:只命中目标 id 的末条,不串锚', async () => {
|
|
173
|
+
const file = writeMainSession('main.jsonl', [
|
|
174
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
175
|
+
recordLine('e1', 'sa-1', { sessionId: 'sa1-r1', dbPath: DB_PATH }),
|
|
176
|
+
recordLine('e2', 'sa-2', { sessionId: 'sa2-r1', dbPath: DB_PATH }),
|
|
177
|
+
recordLine('e3', 'sa-1', { sessionId: 'sa1-r2', dbPath: DB_PATH }),
|
|
178
|
+
recordLine('e4', 'sa-2', { sessionId: 'sa2-r2', dbPath: DB_PATH }),
|
|
179
|
+
recordLine('e5', 'sa-1', { sessionId: 'sa1-r3', dbPath: DB_PATH }),
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
expect((await findZcodeEntryAnchor([file], 'sa-1'))?.sessionId).toBe('sa1-r3')
|
|
183
|
+
expect((await findZcodeEntryAnchor([file], 'sa-2'))?.sessionId).toBe('sa2-r2')
|
|
184
|
+
})
|
|
188
185
|
})
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
186
|
+
|
|
187
|
+
describe('行级容错(best-effort,不因一行坏 JSON 失败整文件)', () => {
|
|
188
|
+
it('坏行穿插 + 末行半行:正常命中末条完整条目', async () => {
|
|
189
|
+
const file = writeMainSession('main.jsonl', [
|
|
190
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
191
|
+
'not-json-at-all{{{',
|
|
192
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-A', dbPath: DB_PATH }),
|
|
193
|
+
'{"type":"custom","customType":"subagent-record","data":{"broken"',
|
|
194
|
+
])
|
|
195
|
+
|
|
196
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
197
|
+
expect(anchor?.sessionId).toBe('sess-A')
|
|
198
|
+
})
|
|
201
199
|
})
|
|
202
200
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
describe('形状校验窄而严:缺键条目不算命中,继续找更早条目', () => {
|
|
202
|
+
it('末条 sessionRef 缺 dbPath → 命中更早的完整条目', async () => {
|
|
203
|
+
const file = writeMainSession('main.jsonl', [
|
|
204
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
205
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-EARLY', dbPath: DB_PATH }),
|
|
206
|
+
recordLine('e2', 'sa-1', REF_NO_DBPATH),
|
|
207
|
+
])
|
|
209
208
|
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
210
|
+
expect(anchor?.sessionId).toBe('sess-EARLY')
|
|
211
|
+
})
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
213
|
+
it('全部条目缺双键 → not-found(undefined)', async () => {
|
|
214
|
+
const file = writeMainSession('main.jsonl', [
|
|
215
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
216
|
+
recordLine('e1', 'sa-1', REF_NO_DBPATH),
|
|
217
|
+
recordLine('e2', 'sa-1', REF_NO_SESSION_ID),
|
|
218
|
+
])
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('sessionRef 键为空串 → 不算命中(非空 string 才算锚)', async () => {
|
|
224
|
+
const file = writeMainSession('main.jsonl', [
|
|
225
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
226
|
+
recordLine('e1', 'sa-1', { sessionId: '', dbPath: DB_PATH }),
|
|
227
|
+
recordLine('e2', 'sa-1', { sessionId: 'sess-A', dbPath: '' }),
|
|
228
|
+
])
|
|
229
|
+
|
|
230
|
+
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('data.v 非当前版本(future-v,如 3)不算命中,更早 v=2 条目仍可命中', async () => {
|
|
234
|
+
const file = writeMainSession('main.jsonl', [
|
|
235
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
236
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-A', dbPath: DB_PATH }),
|
|
237
|
+
recordLine('e2', 'sa-1', { sessionId: 'sess-V3', dbPath: DB_PATH }, 3),
|
|
238
|
+
])
|
|
239
|
+
|
|
240
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
241
|
+
expect(anchor?.sessionId).toBe('sess-A')
|
|
242
|
+
expect(anchor?.sessionId).not.toBe('sess-V3')
|
|
243
|
+
})
|
|
221
244
|
})
|
|
222
245
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
246
|
+
// ============================================================
|
|
247
|
+
// v2 终态条锚定(W1 D1 版本门补齐:sessionRef 双键取自终态条)
|
|
248
|
+
// ============================================================
|
|
249
|
+
|
|
250
|
+
describe('v2 终态条锚定:kind="settled" + engineHandle.sessionRef 双键', () => {
|
|
251
|
+
it('v2 终态条完整锚(sessionRef 双键齐)→ 命中', async () => {
|
|
252
|
+
const file = writeMainSession('main.jsonl', [
|
|
253
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
254
|
+
messageLine('m1', 'dispatch'),
|
|
255
|
+
v2SettledLine('e1', 'sa-1', {
|
|
256
|
+
engine: 'zcode',
|
|
257
|
+
sessionRef: { sessionId: 'sess-V2A', dbPath: DB_PATH },
|
|
258
|
+
}),
|
|
259
|
+
])
|
|
260
|
+
|
|
261
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
262
|
+
expect(anchor).toBeDefined()
|
|
263
|
+
expect(anchor?.sessionId).toBe('sess-V2A')
|
|
264
|
+
expect(anchor?.dbPath).toBe(DB_PATH)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('v2 注册条(kind="registered",不携带 engineHandle)→ 不命中(锚只取自终态条)', async () => {
|
|
268
|
+
const file = writeMainSession('main.jsonl', [
|
|
269
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
270
|
+
v2RegisteredLine('e-reg', 'sa-1'),
|
|
271
|
+
])
|
|
229
272
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
273
|
+
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('同 id 两条 v2 终态条(早/晚)→ 末条胜(末条锚定语义)', async () => {
|
|
277
|
+
const file = writeMainSession('main.jsonl', [
|
|
278
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
279
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-OLD', dbPath: DB_PATH }),
|
|
280
|
+
v2SettledLine('e2', 'sa-1', {
|
|
281
|
+
engine: 'zcode',
|
|
282
|
+
sessionRef: { sessionId: 'sess-NEW', dbPath: DB_PATH },
|
|
283
|
+
}),
|
|
284
|
+
])
|
|
285
|
+
|
|
286
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
287
|
+
expect(anchor?.sessionId).toBe('sess-NEW')
|
|
288
|
+
expect(anchor?.sessionId).not.toBe('sess-OLD')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('v2 终态条锚残缺(缺 engineHandle)→ 不命中,更早完整条目仍可命中(缺键不遮蔽)', async () => {
|
|
292
|
+
const file = writeMainSession('main.jsonl', [
|
|
293
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
294
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-EARLY', dbPath: DB_PATH }),
|
|
295
|
+
v2SettledLine('e2', 'sa-1', { engine: 'zcode' }), // 无 sessionRef
|
|
296
|
+
])
|
|
297
|
+
|
|
298
|
+
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
299
|
+
expect(anchor?.sessionId).toBe('sess-EARLY')
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('v2 但 kind 不在词表(unknown-kind,非 registered/settled)→ 不命中', async () => {
|
|
303
|
+
const badKind = JSON.stringify({
|
|
304
|
+
type: 'custom',
|
|
305
|
+
customType: 'subagent-record',
|
|
306
|
+
id: 'e-bad',
|
|
307
|
+
parentId: null,
|
|
308
|
+
data: {
|
|
309
|
+
v: 2,
|
|
310
|
+
kind: 'migrated',
|
|
311
|
+
id: 'sa-1',
|
|
312
|
+
engineHandle: { sessionRef: { sessionId: 'sess-BAD', dbPath: DB_PATH }, poolKey: 'shared' },
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
const file = writeMainSession('main.jsonl', [
|
|
316
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
317
|
+
badKind,
|
|
318
|
+
])
|
|
319
|
+
|
|
320
|
+
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
321
|
+
})
|
|
233
322
|
})
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
expect(anchor?.sessionId).toBe('sess-V2A')
|
|
254
|
-
expect(anchor?.dbPath).toBe(DB_PATH)
|
|
323
|
+
|
|
324
|
+
describe('无关条目不命中', () => {
|
|
325
|
+
it('非 subagent-record 的 custom entry、id 不匹配的 record 均跳过', async () => {
|
|
326
|
+
const todoLine = JSON.stringify({
|
|
327
|
+
type: 'custom',
|
|
328
|
+
customType: 'todo',
|
|
329
|
+
id: 't1',
|
|
330
|
+
parentId: null,
|
|
331
|
+
data: { v: 1, id: 'sa-1', engineHandle: { sessionRef: { sessionId: 'sess-TODO', dbPath: DB_PATH } } },
|
|
332
|
+
})
|
|
333
|
+
const file = writeMainSession('main.jsonl', [
|
|
334
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
335
|
+
todoLine,
|
|
336
|
+
recordLine('e1', 'sa-other', { sessionId: 'sess-OTHER', dbPath: DB_PATH }),
|
|
337
|
+
])
|
|
338
|
+
|
|
339
|
+
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
340
|
+
expect((await findZcodeEntryAnchor([file], 'sa-other'))?.sessionId).toBe('sess-OTHER')
|
|
341
|
+
})
|
|
255
342
|
})
|
|
256
343
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
JSON.stringify({ type: 'session', id: '
|
|
260
|
-
|
|
261
|
-
|
|
344
|
+
describe('候选文件列表:顺序回退 + 命中即止(越界语义由调用方承载)', () => {
|
|
345
|
+
it('前一候选未命中 → 后一候选命中', async () => {
|
|
346
|
+
const empty = writeMainSession('empty.jsonl', [JSON.stringify({ type: 'session', id: 's0' })])
|
|
347
|
+
const withAnchor = writeMainSession('with-anchor.jsonl', [
|
|
348
|
+
JSON.stringify({ type: 'session', id: 'main-2' }),
|
|
349
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-B', dbPath: DB_PATH }),
|
|
350
|
+
])
|
|
262
351
|
|
|
263
|
-
|
|
264
|
-
|
|
352
|
+
const anchor: ZcodeAnchor | undefined = await findZcodeEntryAnchor([empty, withAnchor], 'sa-1')
|
|
353
|
+
expect(anchor?.sessionId).toBe('sess-B')
|
|
354
|
+
})
|
|
265
355
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
356
|
+
it('前一候选命中即止:不跨文件合并(后一候选的更新条目不覆盖)', async () => {
|
|
357
|
+
const first = writeMainSession('first.jsonl', [
|
|
358
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
359
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-A', dbPath: DB_PATH }),
|
|
360
|
+
])
|
|
361
|
+
const second = writeMainSession('second.jsonl', [
|
|
362
|
+
JSON.stringify({ type: 'session', id: 'main-2' }),
|
|
363
|
+
recordLine('e2', 'sa-1', { sessionId: 'sess-B', dbPath: DB_PATH }),
|
|
364
|
+
])
|
|
365
|
+
|
|
366
|
+
const anchor = await findZcodeEntryAnchor([first, second], 'sa-1')
|
|
367
|
+
expect(anchor?.sessionId).toBe('sess-A')
|
|
368
|
+
expect(anchor?.sessionId).not.toBe('sess-B')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('候选不可读(不存在)→ 跳过继续,全不可读 → not-found', async () => {
|
|
372
|
+
const withAnchor = writeMainSession('with-anchor.jsonl', [
|
|
373
|
+
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
374
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-B', dbPath: DB_PATH }),
|
|
375
|
+
])
|
|
275
376
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
377
|
+
expect((await findZcodeEntryAnchor([join(dir, 'missing.jsonl'), withAnchor], 'sa-1'))?.sessionId).toBe('sess-B')
|
|
378
|
+
expect(await findZcodeEntryAnchor([join(dir, 'missing.jsonl')], 'sa-1')).toBeUndefined()
|
|
379
|
+
expect(await findZcodeEntryAnchor([], 'sa-1')).toBeUndefined()
|
|
380
|
+
})
|
|
279
381
|
})
|
|
280
382
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
])
|
|
383
|
+
describe('协议字面量漂移检查(跨包契约)', () => {
|
|
384
|
+
it('entry-anchor 本地 customType 字面量 === subagent-core 写侧单源', () => {
|
|
385
|
+
// 源码本地持有磁盘协议字符串(生产依赖范围不引 subagent-core),此处锚定两侧同值
|
|
386
|
+
expect(SUBAGENT_RECORD_CUSTOM_TYPE).toBe('subagent-record')
|
|
387
|
+
})
|
|
287
388
|
|
|
288
|
-
|
|
289
|
-
|
|
389
|
+
it('entry-anchor 本地 v2 认识版本 === subagent-core 写侧单源(写侧再升版本本检查先红)', () => {
|
|
390
|
+
expect(SUBAGENT_RECORD_ENTRY_VERSION).toBe(2)
|
|
391
|
+
})
|
|
290
392
|
})
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ======== 来源:zcode-manifest.test.ts ========
|
|
396
|
+
describe("zcode-manifest", () => {
|
|
397
|
+
// ============================================================
|
|
398
|
+
// 变体 M(设计 §4 场景 1a):zcode manifest 直读(§3.1 第①步主路径)+ 枚举(family 用)。
|
|
399
|
+
//
|
|
400
|
+
// fixture 对齐 F21 磁盘真实形态:zcode manifest **无 sessionFile 键**
|
|
401
|
+
// (派生视图函数照写 sessionFile: undefined,JSON.stringify 丢键)、engine:'zcode'、
|
|
402
|
+
// engineHandle.sessionRef 双键齐。
|
|
403
|
+
//
|
|
404
|
+
// 单元边界:只测「认出 zcode manifest + 提取锚」;白名单门禁 / 错误码映射(U9)、
|
|
405
|
+
// entry 兜底定位语义(U7 已交付)不在本文件展开。
|
|
406
|
+
// ============================================================
|
|
407
|
+
|
|
408
|
+
const DB_PATH = '/data/agent/engines/zcode/session-db/db.sqlite'
|
|
409
|
+
|
|
410
|
+
let agentDir: string
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 在存储树分区 <root>/<sid>/subagents/records/ 下写一个 manifest 文件(手写 JSON 字符串,
|
|
414
|
+
* 忠实控制键)。[session-store-layout-inversion D6/单新根] 新布局无 cwd-slug 层,
|
|
415
|
+
* slug 参数消亡(保留签名兼容既有调用点,弃用改名 _slug);分区名固定合法 sid。
|
|
416
|
+
*/
|
|
417
|
+
function writeManifest(_slug: string, name: string, content: string): string {
|
|
418
|
+
const recordsDir = join(dirname(agentDir), 'ext-sessions', 'root-session-test', 'subagents', 'records')
|
|
419
|
+
mkdirSync(recordsDir, { recursive: true })
|
|
420
|
+
const file = join(recordsDir, name)
|
|
421
|
+
writeFileSync(file, content, 'utf8')
|
|
422
|
+
return file
|
|
423
|
+
}
|
|
291
424
|
|
|
292
|
-
|
|
293
|
-
|
|
425
|
+
/** 磁盘真实形态的 zcode manifest(无 sessionFile 键——F21) */
|
|
426
|
+
function zcodeManifestJson(opts: {
|
|
427
|
+
id: string
|
|
428
|
+
sessionId?: string
|
|
429
|
+
dbPath?: string
|
|
430
|
+
withEngineHandle?: boolean
|
|
431
|
+
engine?: string
|
|
432
|
+
rich?: Record<string, unknown>
|
|
433
|
+
}): string {
|
|
434
|
+
const m: Record<string, unknown> = {
|
|
435
|
+
id: opts.id,
|
|
436
|
+
rootSessionId: 'root-1',
|
|
437
|
+
agentName: 'worker',
|
|
438
|
+
status: 'running',
|
|
439
|
+
...opts.rich,
|
|
440
|
+
}
|
|
441
|
+
// 缺省 zcode(F21 磁盘真实形态);显式传 'pi' 可覆盖
|
|
442
|
+
m.engine = opts.engine ?? 'zcode'
|
|
443
|
+
if (opts.withEngineHandle !== false) {
|
|
444
|
+
m.engineHandle = {
|
|
445
|
+
sessionRef: { sessionId: opts.sessionId ?? 'sess-1', dbPath: opts.dbPath ?? DB_PATH },
|
|
446
|
+
poolKey: 'shared',
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// 刻意不写 sessionFile 键(F21:JSON.stringify 丢 undefined 键的产物形态)
|
|
450
|
+
return JSON.stringify(m)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** 变体 M instrument 用:主 session 文件里的 subagent-record entry 行(U7 recordLine 同款) */
|
|
454
|
+
function recordLine(saId: string, sessionRef: Record<string, string>): string {
|
|
455
|
+
return JSON.stringify({
|
|
294
456
|
type: 'custom',
|
|
295
457
|
customType: 'subagent-record',
|
|
296
|
-
id:
|
|
458
|
+
id: `e-${saId}`,
|
|
297
459
|
parentId: null,
|
|
298
460
|
data: {
|
|
299
|
-
v: 2,
|
|
300
|
-
|
|
301
|
-
id: 'sa-1',
|
|
302
|
-
engineHandle: { sessionRef: { sessionId: 'sess-BAD', dbPath: DB_PATH }, poolKey: 'shared' },
|
|
461
|
+
v: 2, kind: 'settled', id: saId, status: 'idle', stopReason: 'completed', endedAt: 2,
|
|
462
|
+
turns: 1, totalTokens: 10, engine: 'zcode', engineHandle: { sessionRef, poolKey: 'shared' },
|
|
303
463
|
},
|
|
304
464
|
})
|
|
305
|
-
|
|
306
|
-
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
307
|
-
badKind,
|
|
308
|
-
])
|
|
465
|
+
}
|
|
309
466
|
|
|
310
|
-
|
|
467
|
+
beforeEach(() => {
|
|
468
|
+
// agentDir 两层化(<mkdtemp>/agent):存储树推导 = dirname(agentDir)/ext-sessions,
|
|
469
|
+
// 单层直接当 agentDir 会把扫描根泄到 tmpdir 公共层(跨测试污染——先例 reaper/family-zcode 同修)
|
|
470
|
+
agentDir = join(mkdtempSync(join(tmpdir(), 'zcode-manifest-test-')), 'agent')
|
|
471
|
+
mkdirSync(agentDir, { recursive: true })
|
|
311
472
|
})
|
|
312
|
-
})
|
|
313
473
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
474
|
+
afterEach(() => {
|
|
475
|
+
rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
describe('readZcodeManifest 直读:有锚 manifest → 取到 sessionRef', () => {
|
|
479
|
+
it('engine zcode + sessionRef 双键齐 → kind=zcode,锚字段正确', async () => {
|
|
480
|
+
writeManifest(
|
|
481
|
+
'proj-a',
|
|
482
|
+
'sa-abc.json',
|
|
483
|
+
zcodeManifestJson({ id: 'sa-abc', sessionId: 'sess_5f3c', dbPath: DB_PATH }),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
487
|
+
|
|
488
|
+
expect(lookup.kind).toBe('zcode')
|
|
489
|
+
if (lookup.kind !== 'zcode') return
|
|
490
|
+
expect(lookup.anchor.sessionId).toBe('sess_5f3c')
|
|
491
|
+
expect(lookup.anchor.dbPath).toBe(DB_PATH)
|
|
322
492
|
})
|
|
323
|
-
const file = writeMainSession('main.jsonl', [
|
|
324
|
-
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
325
|
-
todoLine,
|
|
326
|
-
recordLine('e1', 'sa-other', { sessionId: 'sess-OTHER', dbPath: DB_PATH }),
|
|
327
|
-
])
|
|
328
493
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
JSON.stringify({ type: 'session', id: 'main-2' }),
|
|
339
|
-
recordLine('e1', 'sa-1', { sessionId: 'sess-B', dbPath: DB_PATH }),
|
|
340
|
-
])
|
|
341
|
-
|
|
342
|
-
const anchor: ZcodeAnchor | undefined = await findZcodeEntryAnchor([empty, withAnchor], 'sa-1')
|
|
343
|
-
expect(anchor?.sessionId).toBe('sess-B')
|
|
344
|
-
})
|
|
494
|
+
it('多 cwd 目录窄 walk:目标不在第一个目录 → 后续目录命中(命中即止)', async () => {
|
|
495
|
+
writeManifest('proj-empty', 'sa-other.json', zcodeManifestJson({ id: 'sa-other' }))
|
|
496
|
+
writeManifest(
|
|
497
|
+
'proj-deep',
|
|
498
|
+
'sa-target.json',
|
|
499
|
+
zcodeManifestJson({ id: 'sa-target', sessionId: 'sess-deep', dbPath: DB_PATH }),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-target')
|
|
345
503
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
504
|
+
expect(lookup.kind).toBe('zcode')
|
|
505
|
+
if (lookup.kind === 'zcode') expect(lookup.anchor.sessionId).toBe('sess-deep')
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('同目录存在坏 JSON manifest 不影响直读(不解析其他 manifest)', async () => {
|
|
509
|
+
writeManifest('proj-a', 'sa-broken.json', '{"id":"sa-broken", truncated')
|
|
510
|
+
writeManifest(
|
|
511
|
+
'proj-a',
|
|
512
|
+
'sa-good.json',
|
|
513
|
+
zcodeManifestJson({ id: 'sa-good', sessionId: 'sess-good', dbPath: DB_PATH }),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-good')
|
|
517
|
+
|
|
518
|
+
expect(lookup.kind).toBe('zcode')
|
|
519
|
+
if (lookup.kind === 'zcode') expect(lookup.anchor.sessionId).toBe('sess-good')
|
|
520
|
+
})
|
|
359
521
|
})
|
|
360
522
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
523
|
+
describe('readZcodeManifest 直读:失败信号(U9 错误码映射的归因输入)', () => {
|
|
524
|
+
it('文件不存在(未知 sa-id)→ not-zcode', async () => {
|
|
525
|
+
writeManifest('proj-a', 'sa-abc.json', zcodeManifestJson({ id: 'sa-abc' }))
|
|
526
|
+
|
|
527
|
+
expect((await readZcodeManifest(agentDir, 'sa-missing')).kind).toBe('not-zcode')
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('坏 JSON → not-zcode(残缺 manifest 与今天的 tryReadManifest 丢弃行为等价)', async () => {
|
|
531
|
+
writeManifest('proj-a', 'sa-abc.json', 'not-json-at-all{{{')
|
|
532
|
+
|
|
533
|
+
expect((await readZcodeManifest(agentDir, 'sa-abc')).kind).toBe('not-zcode')
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('engine 缺省(pi 存量形态:有 sessionFile 无 engine)→ not-zcode', async () => {
|
|
537
|
+
writeManifest(
|
|
538
|
+
'proj-a',
|
|
539
|
+
'sa-abc.json',
|
|
540
|
+
JSON.stringify({
|
|
541
|
+
id: 'sa-abc',
|
|
542
|
+
rootSessionId: 'root-1',
|
|
543
|
+
sessionFile: '/sessions/sub.jsonl',
|
|
544
|
+
}),
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
expect((await readZcodeManifest(agentDir, 'sa-abc')).kind).toBe('not-zcode')
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it("engine 显式 'pi' → not-zcode", async () => {
|
|
551
|
+
writeManifest(
|
|
552
|
+
'proj-a',
|
|
553
|
+
'sa-abc.json',
|
|
554
|
+
zcodeManifestJson({
|
|
555
|
+
id: 'sa-abc',
|
|
556
|
+
engine: 'pi',
|
|
557
|
+
rich: { sessionFile: '/sessions/sub.jsonl' },
|
|
558
|
+
}),
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
expect((await readZcodeManifest(agentDir, 'sa-abc')).kind).toBe('not-zcode')
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('engineHandle 整体缺席 → anchor-missing + reason=missing-engineHandle(旧版本产物 / 孤儿 manifest)', async () => {
|
|
565
|
+
writeManifest(
|
|
566
|
+
'proj-a',
|
|
567
|
+
'sa-abc.json',
|
|
568
|
+
zcodeManifestJson({ id: 'sa-abc', engine: 'zcode', withEngineHandle: false }),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
572
|
+
expect(lookup.kind).toBe('anchor-missing')
|
|
573
|
+
if (lookup.kind === 'anchor-missing') expect(lookup.reason).toBe('missing-engineHandle')
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('sessionRef 整体缺席 → anchor-missing + reason=missing-sessionRef', async () => {
|
|
577
|
+
writeManifest(
|
|
578
|
+
'proj-a',
|
|
579
|
+
'sa-abc.json',
|
|
580
|
+
JSON.stringify({ id: 'sa-abc', rootSessionId: 'root-1', engine: 'zcode', engineHandle: { poolKey: 'shared' } }),
|
|
581
|
+
)
|
|
366
582
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
583
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
584
|
+
expect(lookup.kind).toBe('anchor-missing')
|
|
585
|
+
if (lookup.kind === 'anchor-missing') expect(lookup.reason).toBe('missing-sessionRef')
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
it('sessionRef 缺 dbPath 键 → anchor-missing + reason=missing-dbPath(部分回填的残余形态)', async () => {
|
|
589
|
+
// 手写 JSON:sessionRef 只有 sessionId 单键(zcodeManifestJson 的 ?? 兜底不适用此形态)
|
|
590
|
+
writeManifest(
|
|
591
|
+
'proj-a',
|
|
592
|
+
'sa-abc.json',
|
|
593
|
+
JSON.stringify({
|
|
594
|
+
id: 'sa-abc',
|
|
595
|
+
rootSessionId: 'root-1',
|
|
596
|
+
engine: 'zcode',
|
|
597
|
+
engineHandle: { sessionRef: { sessionId: 'sess-1' }, poolKey: 'shared' },
|
|
598
|
+
}),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
602
|
+
expect(lookup.kind).toBe('anchor-missing')
|
|
603
|
+
if (lookup.kind === 'anchor-missing') expect(lookup.reason).toBe('missing-dbPath')
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('sessionRef 键为空串 → anchor-missing + reason=missing-sessionId(非空 string 才算锚)', async () => {
|
|
607
|
+
writeManifest(
|
|
608
|
+
'proj-a',
|
|
609
|
+
'sa-abc.json',
|
|
610
|
+
zcodeManifestJson({ id: 'sa-abc', sessionId: '', dbPath: DB_PATH }),
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
614
|
+
expect(lookup.kind).toBe('anchor-missing')
|
|
615
|
+
if (lookup.kind === 'anchor-missing') expect(lookup.reason).toBe('missing-sessionId')
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('subagents 根目录不存在 → not-zcode(不抛异常)', async () => {
|
|
619
|
+
const bareDir = mkdtempSync(join(tmpdir(), 'zcode-manifest-bare-'))
|
|
620
|
+
try {
|
|
621
|
+
expect((await readZcodeManifest(bareDir, 'sa-abc')).kind).toBe('not-zcode')
|
|
622
|
+
} finally {
|
|
623
|
+
rmSync(bareDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
624
|
+
}
|
|
625
|
+
})
|
|
370
626
|
})
|
|
371
|
-
})
|
|
372
627
|
|
|
373
|
-
describe('
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
628
|
+
describe('变体 M 定位分支 instrument:manifest 主路径命中(结果来源标记)', () => {
|
|
629
|
+
it('manifest 与 entry 同 sa-id 各持不同锚 → 直读返回 manifest 的锚(entry 兜底来源未被采信)', async () => {
|
|
630
|
+
// 结果来源标记:两来源锚值刻意不同——若实现(未来的路由编排)错误地跳过主路径
|
|
631
|
+
// 落到 entry 兜底,将得到 'sess-from-entry',本断言即失败(走对路而非碰巧对)。
|
|
632
|
+
writeManifest(
|
|
633
|
+
'proj-a',
|
|
634
|
+
'sa-inst.json',
|
|
635
|
+
zcodeManifestJson({ id: 'sa-inst', sessionId: 'sess-from-manifest', dbPath: DB_PATH }),
|
|
636
|
+
)
|
|
637
|
+
const mainFile = join(agentDir, 'main.jsonl')
|
|
638
|
+
writeFileSync(
|
|
639
|
+
mainFile,
|
|
640
|
+
[
|
|
641
|
+
JSON.stringify({ type: 'session', id: 'main-1', cwd: '/proj' }),
|
|
642
|
+
recordLine('sa-inst', { sessionId: 'sess-from-entry', dbPath: DB_PATH }),
|
|
643
|
+
].join('\n') + '\n',
|
|
644
|
+
'utf8',
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
// 主路径命中:锚来自 manifest
|
|
648
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-inst')
|
|
649
|
+
expect(lookup.kind).toBe('zcode')
|
|
650
|
+
if (lookup.kind === 'zcode') {
|
|
651
|
+
expect(lookup.anchor.sessionId).toBe('sess-from-manifest')
|
|
652
|
+
expect(lookup.anchor.sessionId).not.toBe('sess-from-entry')
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// 标记有效性自证:entry 兜底(U7 函数)在同 fixture 上返回的是 entry 来源的锚——
|
|
656
|
+
// 两来源可区分,主路径断言非平凡
|
|
657
|
+
const entryAnchor = await findZcodeEntryAnchor([mainFile], 'sa-inst')
|
|
658
|
+
expect(entryAnchor?.sessionId).toBe('sess-from-entry')
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
it('manifest 缺位 → not-zcode(此情形才需要 entry 兜底介入——定位链分支边界)', async () => {
|
|
662
|
+
expect((await readZcodeManifest(agentDir, 'sa-none')).kind).toBe('not-zcode')
|
|
663
|
+
})
|
|
377
664
|
})
|
|
378
665
|
|
|
379
|
-
|
|
380
|
-
|
|
666
|
+
describe('listZcodeManifests 枚举(family 节点列表来源)', () => {
|
|
667
|
+
it('混排目录只返回 zcode 形态:pi manifest / 坏 JSON / 无锚 zcode 全部跳过', async () => {
|
|
668
|
+
writeManifest(
|
|
669
|
+
'proj-a',
|
|
670
|
+
'sa-z1.json',
|
|
671
|
+
zcodeManifestJson({ id: 'sa-z1', sessionId: 'sess-z1', dbPath: DB_PATH }),
|
|
672
|
+
)
|
|
673
|
+
// pi 存量形态(无 engine、有 sessionFile)——枚举不吸入(isRecordManifest 链零改动)
|
|
674
|
+
writeManifest(
|
|
675
|
+
'proj-a',
|
|
676
|
+
'sa-pi.json',
|
|
677
|
+
JSON.stringify({
|
|
678
|
+
id: 'sa-pi',
|
|
679
|
+
rootSessionId: 'root-1',
|
|
680
|
+
sessionFile: '/sessions/pi-sub.jsonl',
|
|
681
|
+
agentName: 'explorer',
|
|
682
|
+
}),
|
|
683
|
+
)
|
|
684
|
+
// engine:'pi' 显式
|
|
685
|
+
writeManifest(
|
|
686
|
+
'proj-a',
|
|
687
|
+
'sa-pi2.json',
|
|
688
|
+
zcodeManifestJson({ id: 'sa-pi2', engine: 'pi', rich: { sessionFile: '/s.jsonl' } }),
|
|
689
|
+
)
|
|
690
|
+
// zcode 但无锚 → 不产出(直读侧归 anchor-missing,枚举侧无节点可建)
|
|
691
|
+
writeManifest(
|
|
692
|
+
'proj-a',
|
|
693
|
+
'sa-noanchor.json',
|
|
694
|
+
zcodeManifestJson({ id: 'sa-noanchor', engine: 'zcode', withEngineHandle: false }),
|
|
695
|
+
)
|
|
696
|
+
// 坏 JSON → 跳过不中断
|
|
697
|
+
writeManifest('proj-a', 'sa-bad.json', '{{{broken')
|
|
698
|
+
// 第二目录的 zcode manifest → 跨目录枚举
|
|
699
|
+
writeManifest(
|
|
700
|
+
'proj-b',
|
|
701
|
+
'sa-z2.json',
|
|
702
|
+
zcodeManifestJson({ id: 'sa-z2', sessionId: 'sess-z2', dbPath: DB_PATH }),
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
const manifests = await listZcodeManifests(agentDir)
|
|
706
|
+
|
|
707
|
+
expect(manifests.map((m) => m.id).sort()).toEqual(['sa-z1', 'sa-z2'])
|
|
708
|
+
const z1 = manifests.find((m) => m.id === 'sa-z1')
|
|
709
|
+
expect(z1?.engine).toBe('zcode')
|
|
710
|
+
expect(z1?.dbPath).toBe(DB_PATH)
|
|
711
|
+
expect(z1?.rootSessionId).toBe('root-1')
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
it('dbFileExists 预计算:dbPath 指向真实文件 → true;不存在路径 → false(stat 不开库)', async () => {
|
|
715
|
+
const realDb = join(agentDir, 'real-db.sqlite')
|
|
716
|
+
writeFileSync(realDb, 'SQLite format 3 stub', 'utf8')
|
|
717
|
+
writeManifest(
|
|
718
|
+
'proj-a',
|
|
719
|
+
'sa-alive.json',
|
|
720
|
+
zcodeManifestJson({ id: 'sa-alive', sessionId: 'sess-a', dbPath: realDb }),
|
|
721
|
+
)
|
|
722
|
+
writeManifest(
|
|
723
|
+
'proj-a',
|
|
724
|
+
'sa-gone.json',
|
|
725
|
+
zcodeManifestJson({ id: 'sa-gone', sessionId: 'sess-g', dbPath: join(agentDir, 'missing-db.sqlite') }),
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
const manifests = await listZcodeManifests(agentDir)
|
|
729
|
+
|
|
730
|
+
expect(manifests.find((m) => m.id === 'sa-alive')?.dbFileExists).toBe(true)
|
|
731
|
+
expect(manifests.find((m) => m.id === 'sa-gone')?.dbFileExists).toBe(false)
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
it('slug 兜底 m.slug ?? m.agentName ?? ""(与 pi 孤儿链同款)', async () => {
|
|
735
|
+
writeManifest(
|
|
736
|
+
'proj-a',
|
|
737
|
+
'sa-s1.json',
|
|
738
|
+
zcodeManifestJson({ id: 'sa-s1', sessionId: 'sess-1', rich: { slug: 'explicit-slug' } }),
|
|
739
|
+
)
|
|
740
|
+
writeManifest(
|
|
741
|
+
'proj-a',
|
|
742
|
+
'sa-s2.json',
|
|
743
|
+
zcodeManifestJson({ id: 'sa-s2', sessionId: 'sess-2' }), // 无 slug → 回退 agentName
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
const manifests = await listZcodeManifests(agentDir)
|
|
747
|
+
|
|
748
|
+
expect(manifests.find((m) => m.id === 'sa-s1')?.slug).toBe('explicit-slug')
|
|
749
|
+
expect(manifests.find((m) => m.id === 'sa-s2')?.slug).toBe('worker') // agentName 兜底
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
it('存储树根不存在 → 空数组(不报错;单节点不可达不炸整屏)', async () => {
|
|
753
|
+
// agentDir 两层化:扫描根 = dirname(agentDir)/ext-sessions(mkdtemp 私有层内,防公共层污染)
|
|
754
|
+
const bareDir = join(mkdtempSync(join(tmpdir(), 'zcode-manifest-bare-')), 'agent')
|
|
755
|
+
try {
|
|
756
|
+
expect(await listZcodeManifests(bareDir)).toEqual([])
|
|
757
|
+
} finally {
|
|
758
|
+
rmSync(dirname(bareDir), { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
759
|
+
}
|
|
760
|
+
})
|
|
381
761
|
})
|
|
382
|
-
})
|
|
762
|
+
});
|