@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
|
@@ -0,0 +1,361 @@
|
|
|
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
|
+
|
|
6
|
+
import { readZcodeManifest, listZcodeManifests } from '../discovery/zcode-manifest.js'
|
|
7
|
+
import { findZcodeEntryAnchor } from '../discovery/entry-anchor.js'
|
|
8
|
+
|
|
9
|
+
// ============================================================
|
|
10
|
+
// 变体 M(设计 §4 场景 1a):zcode manifest 直读(§3.1 第①步主路径)+ 枚举(family 用)。
|
|
11
|
+
//
|
|
12
|
+
// fixture 对齐 F21 磁盘真实形态:zcode manifest **无 sessionFile 键**
|
|
13
|
+
// (投影函数照写 sessionFile: undefined,JSON.stringify 丢键)、engine:'zcode'、
|
|
14
|
+
// engineHandle.sessionRef 双键齐。
|
|
15
|
+
//
|
|
16
|
+
// 单元边界:只测「认出 zcode manifest + 提取锚」;白名单闸 / 错误码映射(U9)、
|
|
17
|
+
// entry 兜底定位语义(U7 已交付)不在本文件展开。
|
|
18
|
+
// ============================================================
|
|
19
|
+
|
|
20
|
+
const DB_PATH = '/data/agent/engines/zcode/session-db/db.sqlite'
|
|
21
|
+
|
|
22
|
+
let agentDir: string
|
|
23
|
+
|
|
24
|
+
/** 在 <agentDir>/subagents/<slug>/records/ 下写一个 manifest 文件(手写 JSON 字符串,忠实控制键) */
|
|
25
|
+
function writeManifest(slug: string, name: string, content: string): string {
|
|
26
|
+
const recordsDir = join(agentDir, 'subagents', slug, 'records')
|
|
27
|
+
mkdirSync(recordsDir, { recursive: true })
|
|
28
|
+
const file = join(recordsDir, name)
|
|
29
|
+
writeFileSync(file, content, 'utf8')
|
|
30
|
+
return file
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 磁盘真实形态的 zcode manifest(无 sessionFile 键——F21) */
|
|
34
|
+
function zcodeManifestJson(opts: {
|
|
35
|
+
id: string
|
|
36
|
+
sessionId?: string
|
|
37
|
+
dbPath?: string
|
|
38
|
+
withEngineHandle?: boolean
|
|
39
|
+
engine?: string
|
|
40
|
+
rich?: Record<string, unknown>
|
|
41
|
+
}): string {
|
|
42
|
+
const m: Record<string, unknown> = {
|
|
43
|
+
id: opts.id,
|
|
44
|
+
rootSessionId: 'root-1',
|
|
45
|
+
agentName: 'worker',
|
|
46
|
+
status: 'running',
|
|
47
|
+
...opts.rich,
|
|
48
|
+
}
|
|
49
|
+
// 缺省 zcode(F21 磁盘真实形态);显式传 'pi' 可覆盖
|
|
50
|
+
m.engine = opts.engine ?? 'zcode'
|
|
51
|
+
if (opts.withEngineHandle !== false) {
|
|
52
|
+
m.engineHandle = {
|
|
53
|
+
sessionRef: { sessionId: opts.sessionId ?? 'sess-1', dbPath: opts.dbPath ?? DB_PATH },
|
|
54
|
+
poolKey: 'shared',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// 刻意不写 sessionFile 键(F21:JSON.stringify 丢 undefined 键的产物形态)
|
|
58
|
+
return JSON.stringify(m)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 变体 M instrument 用:主 session 文件里的 subagent-record entry 行(U7 recordLine 同款) */
|
|
62
|
+
function recordLine(saId: string, sessionRef: Record<string, string>): string {
|
|
63
|
+
return JSON.stringify({
|
|
64
|
+
type: 'custom',
|
|
65
|
+
customType: 'subagent-record',
|
|
66
|
+
id: `e-${saId}`,
|
|
67
|
+
parentId: null,
|
|
68
|
+
data: { v: 1, id: saId, engine: 'zcode', engineHandle: { sessionRef, poolKey: 'shared' } },
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
agentDir = mkdtempSync(join(tmpdir(), 'zcode-manifest-test-'))
|
|
74
|
+
})
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('readZcodeManifest 直读:有锚 manifest → 取到 sessionRef', () => {
|
|
80
|
+
it('engine zcode + sessionRef 双键齐 → kind=zcode,锚字段正确', async () => {
|
|
81
|
+
writeManifest(
|
|
82
|
+
'proj-a',
|
|
83
|
+
'sa-abc.json',
|
|
84
|
+
zcodeManifestJson({ id: 'sa-abc', sessionId: 'sess_5f3c', dbPath: DB_PATH }),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
88
|
+
|
|
89
|
+
expect(lookup.kind).toBe('zcode')
|
|
90
|
+
if (lookup.kind !== 'zcode') return
|
|
91
|
+
expect(lookup.anchor.sessionId).toBe('sess_5f3c')
|
|
92
|
+
expect(lookup.anchor.dbPath).toBe(DB_PATH)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('多 cwd 目录窄 walk:目标不在第一个目录 → 后续目录命中(命中即止)', async () => {
|
|
96
|
+
writeManifest('proj-empty', 'sa-other.json', zcodeManifestJson({ id: 'sa-other' }))
|
|
97
|
+
writeManifest(
|
|
98
|
+
'proj-deep',
|
|
99
|
+
'sa-target.json',
|
|
100
|
+
zcodeManifestJson({ id: 'sa-target', sessionId: 'sess-deep', dbPath: DB_PATH }),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-target')
|
|
104
|
+
|
|
105
|
+
expect(lookup.kind).toBe('zcode')
|
|
106
|
+
if (lookup.kind === 'zcode') expect(lookup.anchor.sessionId).toBe('sess-deep')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('同目录存在坏 JSON manifest 不影响直读(不解析其他 manifest)', async () => {
|
|
110
|
+
writeManifest('proj-a', 'sa-broken.json', '{"id":"sa-broken", truncated')
|
|
111
|
+
writeManifest(
|
|
112
|
+
'proj-a',
|
|
113
|
+
'sa-good.json',
|
|
114
|
+
zcodeManifestJson({ id: 'sa-good', sessionId: 'sess-good', dbPath: DB_PATH }),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-good')
|
|
118
|
+
|
|
119
|
+
expect(lookup.kind).toBe('zcode')
|
|
120
|
+
if (lookup.kind === 'zcode') expect(lookup.anchor.sessionId).toBe('sess-good')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('readZcodeManifest 直读:失败信号(U9 错误码映射的归因输入)', () => {
|
|
125
|
+
it('文件不存在(未知 sa-id)→ not-zcode', async () => {
|
|
126
|
+
writeManifest('proj-a', 'sa-abc.json', zcodeManifestJson({ id: 'sa-abc' }))
|
|
127
|
+
|
|
128
|
+
expect((await readZcodeManifest(agentDir, 'sa-missing')).kind).toBe('not-zcode')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('坏 JSON → not-zcode(残缺 manifest 与今天的 tryReadManifest 丢弃行为等价)', async () => {
|
|
132
|
+
writeManifest('proj-a', 'sa-abc.json', 'not-json-at-all{{{')
|
|
133
|
+
|
|
134
|
+
expect((await readZcodeManifest(agentDir, 'sa-abc')).kind).toBe('not-zcode')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('engine 缺省(pi 存量形态:有 sessionFile 无 engine)→ not-zcode', async () => {
|
|
138
|
+
writeManifest(
|
|
139
|
+
'proj-a',
|
|
140
|
+
'sa-abc.json',
|
|
141
|
+
JSON.stringify({
|
|
142
|
+
id: 'sa-abc',
|
|
143
|
+
rootSessionId: 'root-1',
|
|
144
|
+
sessionFile: '/sessions/sub.jsonl',
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
expect((await readZcodeManifest(agentDir, 'sa-abc')).kind).toBe('not-zcode')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("engine 显式 'pi' → not-zcode", async () => {
|
|
152
|
+
writeManifest(
|
|
153
|
+
'proj-a',
|
|
154
|
+
'sa-abc.json',
|
|
155
|
+
zcodeManifestJson({
|
|
156
|
+
id: 'sa-abc',
|
|
157
|
+
engine: 'pi',
|
|
158
|
+
rich: { sessionFile: '/sessions/sub.jsonl' },
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
expect((await readZcodeManifest(agentDir, 'sa-abc')).kind).toBe('not-zcode')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('engineHandle 整体缺席 → anchor-missing + reason=missing-engineHandle(旧版本产物 / 孤儿 manifest)', async () => {
|
|
166
|
+
writeManifest(
|
|
167
|
+
'proj-a',
|
|
168
|
+
'sa-abc.json',
|
|
169
|
+
zcodeManifestJson({ id: 'sa-abc', engine: 'zcode', withEngineHandle: false }),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
173
|
+
expect(lookup.kind).toBe('anchor-missing')
|
|
174
|
+
if (lookup.kind === 'anchor-missing') expect(lookup.reason).toBe('missing-engineHandle')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('sessionRef 整体缺席 → anchor-missing + reason=missing-sessionRef', async () => {
|
|
178
|
+
writeManifest(
|
|
179
|
+
'proj-a',
|
|
180
|
+
'sa-abc.json',
|
|
181
|
+
JSON.stringify({ id: 'sa-abc', rootSessionId: 'root-1', engine: 'zcode', engineHandle: { poolKey: 'shared' } }),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
185
|
+
expect(lookup.kind).toBe('anchor-missing')
|
|
186
|
+
if (lookup.kind === 'anchor-missing') expect(lookup.reason).toBe('missing-sessionRef')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('sessionRef 缺 dbPath 键 → anchor-missing + reason=missing-dbPath(部分回填的残余形态)', async () => {
|
|
190
|
+
// 手写 JSON:sessionRef 只有 sessionId 单键(zcodeManifestJson 的 ?? 兜底不适用此形态)
|
|
191
|
+
writeManifest(
|
|
192
|
+
'proj-a',
|
|
193
|
+
'sa-abc.json',
|
|
194
|
+
JSON.stringify({
|
|
195
|
+
id: 'sa-abc',
|
|
196
|
+
rootSessionId: 'root-1',
|
|
197
|
+
engine: 'zcode',
|
|
198
|
+
engineHandle: { sessionRef: { sessionId: 'sess-1' }, poolKey: 'shared' },
|
|
199
|
+
}),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
203
|
+
expect(lookup.kind).toBe('anchor-missing')
|
|
204
|
+
if (lookup.kind === 'anchor-missing') expect(lookup.reason).toBe('missing-dbPath')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('sessionRef 键为空串 → anchor-missing + reason=missing-sessionId(非空 string 才算锚)', async () => {
|
|
208
|
+
writeManifest(
|
|
209
|
+
'proj-a',
|
|
210
|
+
'sa-abc.json',
|
|
211
|
+
zcodeManifestJson({ id: 'sa-abc', sessionId: '', dbPath: DB_PATH }),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-abc')
|
|
215
|
+
expect(lookup.kind).toBe('anchor-missing')
|
|
216
|
+
if (lookup.kind === 'anchor-missing') expect(lookup.reason).toBe('missing-sessionId')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('subagents 根目录不存在 → not-zcode(不抛异常)', async () => {
|
|
220
|
+
const bareDir = mkdtempSync(join(tmpdir(), 'zcode-manifest-bare-'))
|
|
221
|
+
try {
|
|
222
|
+
expect((await readZcodeManifest(bareDir, 'sa-abc')).kind).toBe('not-zcode')
|
|
223
|
+
} finally {
|
|
224
|
+
rmSync(bareDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('变体 M 定位分支 instrument:manifest 主路径命中(结果来源标记)', () => {
|
|
230
|
+
it('manifest 与 entry 同 sa-id 各持不同锚 → 直读返回 manifest 的锚(entry 兜底来源未被采信)', async () => {
|
|
231
|
+
// 结果来源标记:两来源锚值刻意不同——若实现(未来的路由编排)错误地跳过主路径
|
|
232
|
+
// 落到 entry 兜底,将得到 'sess-from-entry',本断言即失败(走对路而非碰巧对)。
|
|
233
|
+
writeManifest(
|
|
234
|
+
'proj-a',
|
|
235
|
+
'sa-inst.json',
|
|
236
|
+
zcodeManifestJson({ id: 'sa-inst', sessionId: 'sess-from-manifest', dbPath: DB_PATH }),
|
|
237
|
+
)
|
|
238
|
+
const mainFile = join(agentDir, 'main.jsonl')
|
|
239
|
+
writeFileSync(
|
|
240
|
+
mainFile,
|
|
241
|
+
[
|
|
242
|
+
JSON.stringify({ type: 'session', id: 'main-1', cwd: '/proj' }),
|
|
243
|
+
recordLine('sa-inst', { sessionId: 'sess-from-entry', dbPath: DB_PATH }),
|
|
244
|
+
].join('\n') + '\n',
|
|
245
|
+
'utf8',
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// 主路径命中:锚来自 manifest
|
|
249
|
+
const lookup = await readZcodeManifest(agentDir, 'sa-inst')
|
|
250
|
+
expect(lookup.kind).toBe('zcode')
|
|
251
|
+
if (lookup.kind === 'zcode') {
|
|
252
|
+
expect(lookup.anchor.sessionId).toBe('sess-from-manifest')
|
|
253
|
+
expect(lookup.anchor.sessionId).not.toBe('sess-from-entry')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 标记有效性自证:entry 兜底(U7 函数)在同 fixture 上返回的是 entry 来源的锚——
|
|
257
|
+
// 两来源可区分,主路径断言非平凡
|
|
258
|
+
const entryAnchor = await findZcodeEntryAnchor([mainFile], 'sa-inst')
|
|
259
|
+
expect(entryAnchor?.sessionId).toBe('sess-from-entry')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('manifest 缺位 → not-zcode(此情形才需要 entry 兜底介入——定位链分支边界)', async () => {
|
|
263
|
+
expect((await readZcodeManifest(agentDir, 'sa-none')).kind).toBe('not-zcode')
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('listZcodeManifests 枚举(family 节点列表来源)', () => {
|
|
268
|
+
it('混排目录只返回 zcode 形态:pi manifest / 坏 JSON / 无锚 zcode 全部跳过', async () => {
|
|
269
|
+
writeManifest(
|
|
270
|
+
'proj-a',
|
|
271
|
+
'sa-z1.json',
|
|
272
|
+
zcodeManifestJson({ id: 'sa-z1', sessionId: 'sess-z1', dbPath: DB_PATH }),
|
|
273
|
+
)
|
|
274
|
+
// pi 存量形态(无 engine、有 sessionFile)——枚举不吸入(isRecordManifest 链零改动)
|
|
275
|
+
writeManifest(
|
|
276
|
+
'proj-a',
|
|
277
|
+
'sa-pi.json',
|
|
278
|
+
JSON.stringify({
|
|
279
|
+
id: 'sa-pi',
|
|
280
|
+
rootSessionId: 'root-1',
|
|
281
|
+
sessionFile: '/sessions/pi-sub.jsonl',
|
|
282
|
+
agentName: 'explorer',
|
|
283
|
+
}),
|
|
284
|
+
)
|
|
285
|
+
// engine:'pi' 显式
|
|
286
|
+
writeManifest(
|
|
287
|
+
'proj-a',
|
|
288
|
+
'sa-pi2.json',
|
|
289
|
+
zcodeManifestJson({ id: 'sa-pi2', engine: 'pi', rich: { sessionFile: '/s.jsonl' } }),
|
|
290
|
+
)
|
|
291
|
+
// zcode 但无锚 → 不产出(直读侧归 anchor-missing,枚举侧无节点可建)
|
|
292
|
+
writeManifest(
|
|
293
|
+
'proj-a',
|
|
294
|
+
'sa-noanchor.json',
|
|
295
|
+
zcodeManifestJson({ id: 'sa-noanchor', engine: 'zcode', withEngineHandle: false }),
|
|
296
|
+
)
|
|
297
|
+
// 坏 JSON → 跳过不中断
|
|
298
|
+
writeManifest('proj-a', 'sa-bad.json', '{{{broken')
|
|
299
|
+
// 第二目录的 zcode manifest → 跨目录枚举
|
|
300
|
+
writeManifest(
|
|
301
|
+
'proj-b',
|
|
302
|
+
'sa-z2.json',
|
|
303
|
+
zcodeManifestJson({ id: 'sa-z2', sessionId: 'sess-z2', dbPath: DB_PATH }),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
const manifests = await listZcodeManifests(agentDir)
|
|
307
|
+
|
|
308
|
+
expect(manifests.map((m) => m.id).sort()).toEqual(['sa-z1', 'sa-z2'])
|
|
309
|
+
const z1 = manifests.find((m) => m.id === 'sa-z1')
|
|
310
|
+
expect(z1?.engine).toBe('zcode')
|
|
311
|
+
expect(z1?.dbPath).toBe(DB_PATH)
|
|
312
|
+
expect(z1?.rootSessionId).toBe('root-1')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('dbFileExists 预计算:dbPath 指向真实文件 → true;不存在路径 → false(stat 不开库)', async () => {
|
|
316
|
+
const realDb = join(agentDir, 'real-db.sqlite')
|
|
317
|
+
writeFileSync(realDb, 'SQLite format 3 stub', 'utf8')
|
|
318
|
+
writeManifest(
|
|
319
|
+
'proj-a',
|
|
320
|
+
'sa-alive.json',
|
|
321
|
+
zcodeManifestJson({ id: 'sa-alive', sessionId: 'sess-a', dbPath: realDb }),
|
|
322
|
+
)
|
|
323
|
+
writeManifest(
|
|
324
|
+
'proj-a',
|
|
325
|
+
'sa-gone.json',
|
|
326
|
+
zcodeManifestJson({ id: 'sa-gone', sessionId: 'sess-g', dbPath: join(agentDir, 'missing-db.sqlite') }),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
const manifests = await listZcodeManifests(agentDir)
|
|
330
|
+
|
|
331
|
+
expect(manifests.find((m) => m.id === 'sa-alive')?.dbFileExists).toBe(true)
|
|
332
|
+
expect(manifests.find((m) => m.id === 'sa-gone')?.dbFileExists).toBe(false)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('slug 兜底 m.slug ?? m.agentName ?? ""(与 pi 孤儿链同款)', async () => {
|
|
336
|
+
writeManifest(
|
|
337
|
+
'proj-a',
|
|
338
|
+
'sa-s1.json',
|
|
339
|
+
zcodeManifestJson({ id: 'sa-s1', sessionId: 'sess-1', rich: { slug: 'explicit-slug' } }),
|
|
340
|
+
)
|
|
341
|
+
writeManifest(
|
|
342
|
+
'proj-a',
|
|
343
|
+
'sa-s2.json',
|
|
344
|
+
zcodeManifestJson({ id: 'sa-s2', sessionId: 'sess-2' }), // 无 slug → 回退 agentName
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const manifests = await listZcodeManifests(agentDir)
|
|
348
|
+
|
|
349
|
+
expect(manifests.find((m) => m.id === 'sa-s1')?.slug).toBe('explicit-slug')
|
|
350
|
+
expect(manifests.find((m) => m.id === 'sa-s2')?.slug).toBe('worker') // agentName 兜底
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('subagents 根不存在 → 空数组(不报错;单节点不可达不炸整屏)', async () => {
|
|
354
|
+
const bareDir = mkdtempSync(join(tmpdir(), 'zcode-manifest-bare-'))
|
|
355
|
+
try {
|
|
356
|
+
expect(await listZcodeManifests(bareDir)).toEqual([])
|
|
357
|
+
} finally {
|
|
358
|
+
rmSync(bareDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
})
|