@zhushanwen/pi-session-reader 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/__tests__/cross-package-subagent-core.test.ts +321 -0
- package/src/__tests__/execution-tree.test.ts +1 -1
- package/src/__tests__/find.test.ts +3 -3
- package/src/__tests__/hash-provider.test.ts +2 -2
- package/src/__tests__/result.test.ts +341 -0
- package/src/__tests__/roots.test.ts +2 -2
- package/src/__tests__/session-command.test.ts +2 -2
- package/src/__tests__/subagents.test.ts +3 -3
- package/src/discovery/subagents.ts +7 -5
- package/src/index.ts +20 -5
- package/src/result-action.ts +244 -0
- package/src/tool-handler.ts +87 -53
- package/vitest.config.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-session-reader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"xyz-agent": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@vitest/coverage-v8": "^4.1.9",
|
|
20
|
-
"vitest": "^4.1.8"
|
|
20
|
+
"vitest": "^4.1.8",
|
|
21
|
+
"@zhushanwen/subagent-core": "0.6.0"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
24
|
"index.ts",
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as os from 'node:os'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { handleSessionRead } from '../tool-handler.js'
|
|
7
|
+
// barrel 符号走包根;ManifestStore / ModelRegistryLike / path-encoding 函数未进 barrel
|
|
8
|
+
// (D3 判定不进),按 subagent-workflow 测试同款包名 .ts 深路径消费,vitest alias 解析。
|
|
9
|
+
import {
|
|
10
|
+
ModelConfigService,
|
|
11
|
+
RecordStore,
|
|
12
|
+
SubagentService,
|
|
13
|
+
type SubagentRecord,
|
|
14
|
+
} from '@zhushanwen/subagent-core'
|
|
15
|
+
import { ManifestStore } from '@zhushanwen/subagent-core/execution/manifest-store.ts'
|
|
16
|
+
import type { ModelRegistryLike } from '@zhushanwen/subagent-core/execution/model-resolver.ts'
|
|
17
|
+
import {
|
|
18
|
+
getSubagentRecordsDir,
|
|
19
|
+
getSubagentSessionDir,
|
|
20
|
+
} from '@zhushanwen/subagent-core/execution/path-encoding.ts'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* W4 跨包集成测试(subagent-sync-collect v2 impl-plan W4 验收条款①,设计 §5 W4:
|
|
24
|
+
* 「subagent-core 真实 tmpdir 产出磁盘状态 → session-reader result action sa- id 反查,
|
|
25
|
+
* 不 mock manifest 写入」)。
|
|
26
|
+
*
|
|
27
|
+
* 与 result.test.ts 的分工:result.test.ts 的 fixture **手工预写** manifest(U6 时期
|
|
28
|
+
* W3 投影代码尚不存在);本文件 manifest 内容必须来自 v2 W3 的投影代码路径——
|
|
29
|
+
* subagent-core 侧真实驱动 E1 崩溃恢复(kill -9 同构链路:真实 RecordStore 落盘
|
|
30
|
+
* register/轮终 entry → initSession 内 orphan 覆写真实跑 → recoverSyncCollectBatch
|
|
31
|
+
* 落标出口 appendBatchFinalizedEntry fire-and-forget 真写 records/<sa-id>.json),
|
|
32
|
+
* session-reader 侧零 mock 读同一 tmpdir 断言反查闭环。
|
|
33
|
+
*
|
|
34
|
+
* 构造形态取自 subagent-core sync-collect-recovery.test.ts 的 kill -9 同构主用例
|
|
35
|
+
* (W1 领地,形态同构是 v1 教训「种子绕过真实链路 = 假绿」的执行):子 session 文件
|
|
36
|
+
* 在其基础上补 message entries(result action 的正文提取源)。
|
|
37
|
+
*
|
|
38
|
+
* mock 面最小:仅构造 SubagentService 所需的最小 pi 形状(appendEntry 真写主文件,
|
|
39
|
+
* 其余 no-op stub)——零 vi.mock、notifier/store/config 全真实实现,agentDir 全部
|
|
40
|
+
* mkdtemp(tmpdir) 自建自删(红线:零真实数据目录触碰)。
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** 剥身份/relay env(subagent-core 测试纪律同款):身份 env 会让 service 误判自己是
|
|
44
|
+
* 子进程跳过恢复扫描,relay env 改道 pi-invocation 的 relay 分支。 */
|
|
45
|
+
const ENV_KEYS_TO_STRIP = [
|
|
46
|
+
'PI_SUBAGENT_ROOT_SESSION_ID',
|
|
47
|
+
'PI_SUBAGENT_SELF_RECORD_ID',
|
|
48
|
+
'PI_SUBAGENT_DEPTH',
|
|
49
|
+
'PI_SUBAGENT_ROOT_CWD',
|
|
50
|
+
'PI_SUBAGENT_FORK_DEPTH',
|
|
51
|
+
'XYZ_SUBAGENT_RELAY_SOCKET',
|
|
52
|
+
'XYZ_SUBAGENT_RELAY_NODE',
|
|
53
|
+
'XYZ_SUBAGENT_RELAY_SCRIPT',
|
|
54
|
+
] as const
|
|
55
|
+
|
|
56
|
+
const ROOT_SESSION = 'root-cross-pkg'
|
|
57
|
+
|
|
58
|
+
/** 驱动 SubagentService 的最小 pi 形状:appendEntry 真写主 session JSONL(pi 落盘
|
|
59
|
+
* 形态同构),其余面 no-op(本链路 E1 全终态直达补发,settled 重扫不注册)。 */
|
|
60
|
+
function makeWritingPi(mainFile: string) {
|
|
61
|
+
return {
|
|
62
|
+
appendEntry(customType: string, data: unknown): void {
|
|
63
|
+
fs.appendFileSync(
|
|
64
|
+
mainFile,
|
|
65
|
+
`${JSON.stringify({
|
|
66
|
+
type: 'custom',
|
|
67
|
+
id: `seed-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
68
|
+
parentId: null,
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
customType,
|
|
71
|
+
data,
|
|
72
|
+
})}\n`,
|
|
73
|
+
'utf-8',
|
|
74
|
+
)
|
|
75
|
+
},
|
|
76
|
+
events: { emit(): void {} },
|
|
77
|
+
sendMessage(): void {},
|
|
78
|
+
on(): void {},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 种子成员 record(真实 RecordStore.reportSubagentRecord 入参——序列化/落盘/扫描
|
|
83
|
+
* 三层真实;字段形状同 subagent-core sync-collect-recovery.test.ts memberRecord)。 */
|
|
84
|
+
function memberRecord(overrides: Partial<SubagentRecord> & { id: string }): SubagentRecord {
|
|
85
|
+
return {
|
|
86
|
+
agent: '/agents/worker.md',
|
|
87
|
+
task: 'cross-package task',
|
|
88
|
+
slug: 'cross',
|
|
89
|
+
status: 'running',
|
|
90
|
+
mode: 'background',
|
|
91
|
+
startedAt: 1000,
|
|
92
|
+
rootSessionId: ROOT_SESSION,
|
|
93
|
+
parentRecordId: undefined,
|
|
94
|
+
depth: 0,
|
|
95
|
+
endedAt: undefined,
|
|
96
|
+
turns: 0,
|
|
97
|
+
totalTokens: 0,
|
|
98
|
+
model: 'prov/m1',
|
|
99
|
+
thinkingLevel: undefined,
|
|
100
|
+
eventLog: [],
|
|
101
|
+
displayItems: [],
|
|
102
|
+
result: undefined,
|
|
103
|
+
error: undefined,
|
|
104
|
+
sessionFile: undefined,
|
|
105
|
+
chatMode: false,
|
|
106
|
+
collectMode: 'sync',
|
|
107
|
+
...overrides,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface MemberSeed {
|
|
112
|
+
id: string
|
|
113
|
+
/** 子 session 最终 assistant 正文(跨 message 以 "\n\n" join 后的整体——record.result
|
|
114
|
+
* 与 result action 提取的同源基线,逐字节一致断言锚点)。 */
|
|
115
|
+
resultText: string
|
|
116
|
+
/** 子文件 assistant message 正文分段(跨 message join "\n\n")。 */
|
|
117
|
+
assistantParts: string[]
|
|
118
|
+
model: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe('跨包集成:subagent-core E1 落标 manifest → session-reader result 反查(W4)', () => {
|
|
122
|
+
let agentDir: string
|
|
123
|
+
let mainFile: string
|
|
124
|
+
let services: SubagentService[]
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
for (const k of ENV_KEYS_TO_STRIP) delete process.env[k]
|
|
128
|
+
agentDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cross-pkg-subagent-core-'))
|
|
129
|
+
fs.mkdirSync(getSubagentSessionDir(agentDir, agentDir), { recursive: true })
|
|
130
|
+
mainFile = path.join(agentDir, 'main-session.jsonl')
|
|
131
|
+
services = []
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
for (const s of services) s.dispose()
|
|
136
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
/** 真实 RecordStore(写文件 pi:种子经真实 toSubagentRecordEntry 序列化落主文件)。 */
|
|
140
|
+
function makeSeedStore(): RecordStore {
|
|
141
|
+
return new RecordStore(
|
|
142
|
+
getSubagentSessionDir(agentDir, agentDir),
|
|
143
|
+
new ManifestStore(getSubagentRecordsDir(agentDir, agentDir)),
|
|
144
|
+
makeWritingPi(mainFile),
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 恢复侧真实 SubagentService:initSession 内 orphan 覆写真实跑(覆写 entry 落盘
|
|
149
|
+
* 保留批标记/result),返回 service 供显式驱动 E1。 */
|
|
150
|
+
function makeRecoveryService(): SubagentService {
|
|
151
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir })
|
|
152
|
+
const modelRegistry: ModelRegistryLike = {
|
|
153
|
+
getAvailable: () => [],
|
|
154
|
+
find: () => undefined,
|
|
155
|
+
hasConfiguredAuth: () => false,
|
|
156
|
+
}
|
|
157
|
+
modelService.initModel({
|
|
158
|
+
sessionId: ROOT_SESSION,
|
|
159
|
+
ctxModel: { id: 'm', name: 'M', provider: 'p', reasoning: false },
|
|
160
|
+
modelRegistry,
|
|
161
|
+
})
|
|
162
|
+
const service = new SubagentService({ cwd: agentDir, modelService })
|
|
163
|
+
service.initSession({ pi: makeWritingPi(mainFile), sessionId: ROOT_SESSION, mainSessionFile: mainFile })
|
|
164
|
+
services.push(service)
|
|
165
|
+
return service
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 子 session 文件:header + user 任务 + assistant 正文(message entries 是
|
|
169
|
+
* result action 的提取源)+ identity/record entry(orphan 覆写重建的数据锚,
|
|
170
|
+
* 形态同 subagent-core W1 用例)——**不写**三 sidecar,保证恢复走
|
|
171
|
+
* finalizeOrphanRecord 真实路径。 */
|
|
172
|
+
function writeChildSessionFile(m: MemberSeed): string {
|
|
173
|
+
const childFile = path.join(getSubagentSessionDir(agentDir, agentDir), `${m.id}.jsonl`)
|
|
174
|
+
const ts = new Date(1000).toISOString()
|
|
175
|
+
const lines: string[] = [
|
|
176
|
+
JSON.stringify({ type: 'session', version: 3, id: `sess-${m.id}`, timestamp: ts, cwd: agentDir }),
|
|
177
|
+
JSON.stringify({
|
|
178
|
+
type: 'message', id: `${m.id}-u1`, parentId: `sess-${m.id}`, timestamp: ts,
|
|
179
|
+
message: { role: 'user', content: [{ type: 'text', text: 'do the cross-package task' }] },
|
|
180
|
+
}),
|
|
181
|
+
]
|
|
182
|
+
m.assistantParts.forEach((text, i) => {
|
|
183
|
+
lines.push(
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
type: 'message', id: `${m.id}-a${i + 1}`, parentId: `${m.id}-u1`, timestamp: ts,
|
|
186
|
+
message: {
|
|
187
|
+
role: 'assistant',
|
|
188
|
+
content: [
|
|
189
|
+
{ type: 'thinking', thinking: 'internal reasoning must be excluded' },
|
|
190
|
+
{ type: 'text', text },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
lines.push(
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
type: 'custom', id: `cid-${m.id}-1`, parentId: null, timestamp: ts,
|
|
199
|
+
customType: 'subagent-identity',
|
|
200
|
+
data: { id: m.id, agent: '/agents/worker.md', mode: 'background', task: 'cross-package task', startedAt: 1000, rootSessionId: ROOT_SESSION, depth: 0 },
|
|
201
|
+
}),
|
|
202
|
+
JSON.stringify({
|
|
203
|
+
type: 'custom', id: `cid-${m.id}-2`, parentId: `cid-${m.id}-1`, timestamp: ts,
|
|
204
|
+
customType: 'subagent-record',
|
|
205
|
+
data: {
|
|
206
|
+
v: 1, id: m.id, agent: '/agents/worker.md', task: 'cross-package task', slug: 'cross',
|
|
207
|
+
status: 'running', mode: 'background', startedAt: 1000, rootSessionId: ROOT_SESSION,
|
|
208
|
+
depth: 0, turns: 1, totalTokens: 10, model: m.model, eventLog: [], displayItems: [],
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
fs.writeFileSync(childFile, lines.join('\n') + '\n', 'utf-8')
|
|
213
|
+
return childFile
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 种「崩溃前主文件末条序列」:register(running+sync)→ 轮终(running+resumable+
|
|
217
|
+
* result 全文 + sessionFile)——成功成员崩溃时的真实末条形态。 */
|
|
218
|
+
function seedRoundTerminalEntries(m: MemberSeed, childFile: string): void {
|
|
219
|
+
const store = makeSeedStore()
|
|
220
|
+
store.reportSubagentRecord(memberRecord({ id: m.id, sessionFile: childFile, model: m.model }))
|
|
221
|
+
store.reportSubagentRecord(
|
|
222
|
+
memberRecord({ id: m.id, sessionFile: childFile, model: m.model, resumable: true, result: m.resultText }),
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 手写短轮询(真实 timers,vitest 4.1.8 vi.waitFor 对 falsy callback 不轮询——
|
|
227
|
+
* subagent-core 同款纪律):manifest 为 fire-and-forget 异步写,需轮询等落盘。 */
|
|
228
|
+
async function until(cond: () => boolean, timeoutMs = 3000): Promise<void> {
|
|
229
|
+
const start = Date.now()
|
|
230
|
+
while (!cond()) {
|
|
231
|
+
if (Date.now() - start > timeoutMs) {
|
|
232
|
+
throw new Error(`cross-pkg: condition not met within ${timeoutMs}ms`)
|
|
233
|
+
}
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** 全链路驱动:种子 → 恢复 service(orphan 覆写)→ E1(落标出口真写 manifest)。
|
|
239
|
+
* 返回子 session 文件路径表。 */
|
|
240
|
+
async function driveSyncBatchRecovery(members: MemberSeed[]): Promise<Map<string, string>> {
|
|
241
|
+
const childFiles = new Map<string, string>()
|
|
242
|
+
for (const m of members) {
|
|
243
|
+
const childFile = writeChildSessionFile(m)
|
|
244
|
+
childFiles.set(m.id, childFile)
|
|
245
|
+
seedRoundTerminalEntries(m, childFile)
|
|
246
|
+
}
|
|
247
|
+
const service = makeRecoveryService()
|
|
248
|
+
service.recoverSyncCollectBatch()
|
|
249
|
+
// E1 落标出口 appendBatchFinalizedEntry 的 manifest 是 fire-and-forget best-effort
|
|
250
|
+
// 写——轮询等全部成员 manifest 真实落盘(W3 投影代码路径的产物)。
|
|
251
|
+
await until(() =>
|
|
252
|
+
members.every((m) => fs.existsSync(path.join(getSubagentRecordsDir(agentDir, agentDir), `${m.id}.json`))),
|
|
253
|
+
)
|
|
254
|
+
return childFiles
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
it('1. 单 sa- id:E1 落标 manifest 真实产出 → result 反查命中,正文与 record.result 逐字节一致', async () => {
|
|
258
|
+
const resultText = 'part one\n\npart two'
|
|
259
|
+
const m: MemberSeed = {
|
|
260
|
+
id: 'sa-cross-1',
|
|
261
|
+
resultText,
|
|
262
|
+
assistantParts: ['part one', 'part two'],
|
|
263
|
+
model: 'prov/round-m',
|
|
264
|
+
}
|
|
265
|
+
const childFiles = await driveSyncBatchRecovery([m])
|
|
266
|
+
|
|
267
|
+
// manifest 内容断言(W3 appendBatchFinalizedEntry 投影路径的产物,非手工预写):
|
|
268
|
+
// 覆写后 closed 重建快照 → status 如实投影 "closed";sessionFile 来自 W1 投影扩展。
|
|
269
|
+
const manifest = JSON.parse(
|
|
270
|
+
fs.readFileSync(path.join(getSubagentRecordsDir(agentDir, agentDir), 'sa-cross-1.json'), 'utf-8'),
|
|
271
|
+
) as Record<string, unknown>
|
|
272
|
+
expect(manifest).toMatchObject({
|
|
273
|
+
id: 'sa-cross-1',
|
|
274
|
+
rootSessionId: ROOT_SESSION,
|
|
275
|
+
agentName: '/agents/worker.md',
|
|
276
|
+
status: 'closed',
|
|
277
|
+
sessionFile: childFiles.get('sa-cross-1'),
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// session-reader 侧:sa- id 反查(v1 此处报「无匹配 record」——manifest 惰性不落盘)
|
|
281
|
+
const r = await handleSessionRead({ action: 'result', session: 'sa-cross-1' }, agentDir)
|
|
282
|
+
// 同源语义:跨 assistant message "\n\n" join、thinking 块排除——与 record.result 逐字节一致
|
|
283
|
+
expect(r.content[0]?.text).toBe(resultText)
|
|
284
|
+
const d = r.details as {
|
|
285
|
+
session: string
|
|
286
|
+
sessionId: string
|
|
287
|
+
sessionFile: string
|
|
288
|
+
totalChars: number
|
|
289
|
+
truncated: boolean
|
|
290
|
+
}
|
|
291
|
+
expect(d.session).toBe('sa-cross-1')
|
|
292
|
+
expect(d.sessionId).toBe('sess-sa-cross-1') // header 真实 id,非 sa- 占位
|
|
293
|
+
expect(d.sessionFile).toBe(childFiles.get('sa-cross-1'))
|
|
294
|
+
expect(d.totalChars).toBe(resultText.length)
|
|
295
|
+
expect(d.truncated).toBe(false)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('2. 双成员批(逗号串批量):全部 sa- id 反查命中,头行/分隔/顺序与正文一致', async () => {
|
|
299
|
+
const members: MemberSeed[] = [
|
|
300
|
+
{ id: 'sa-cross-a', resultText: 'alpha final answer', assistantParts: ['alpha final answer'], model: 'prov/ma' },
|
|
301
|
+
{ id: 'sa-cross-b', resultText: 'beta final answer', assistantParts: ['beta final answer'], model: 'prov/mb' },
|
|
302
|
+
]
|
|
303
|
+
await driveSyncBatchRecovery(members)
|
|
304
|
+
|
|
305
|
+
const r = await handleSessionRead({ action: 'result', session: 'sa-cross-a,sa-cross-b' }, agentDir)
|
|
306
|
+
const text = r.content[0]?.text ?? ''
|
|
307
|
+
expect(text).toContain('[1/2] sa-cross-a')
|
|
308
|
+
expect(text).toContain('[2/2] sa-cross-b')
|
|
309
|
+
expect(text.indexOf('[1/2]')).toBeLessThan(text.indexOf('[2/2]'))
|
|
310
|
+
expect(text).toContain('\n\n---\n\n')
|
|
311
|
+
// 每成员正文与 record.result 一致(批量形态下同样逐字节)
|
|
312
|
+
expect(text).toContain('alpha final answer')
|
|
313
|
+
expect(text).toContain('beta final answer')
|
|
314
|
+
const d = r.details as { count: number; items: Array<{ session: string; text: string; truncated: boolean }> }
|
|
315
|
+
expect(d.count).toBe(2)
|
|
316
|
+
expect(d.items.map((i) => i.session)).toEqual(['sa-cross-a', 'sa-cross-b'])
|
|
317
|
+
expect(d.items[0]?.text).toBe('alpha final answer')
|
|
318
|
+
expect(d.items[1]?.text).toBe('beta final answer')
|
|
319
|
+
expect(d.items.every((i) => !i.truncated)).toBe(true)
|
|
320
|
+
})
|
|
321
|
+
})
|
|
@@ -188,7 +188,7 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
188
188
|
dir = await makeAgentDir()
|
|
189
189
|
})
|
|
190
190
|
afterEach(async () => {
|
|
191
|
-
await rm(dir, { recursive: true, force: true })
|
|
191
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
192
192
|
})
|
|
193
193
|
|
|
194
194
|
it('TC-m3b-nested-tree:main→subagent A→{workflow-call B, subagent C} 相互嵌套(parentRecordId 精确链)', async () => {
|
|
@@ -43,9 +43,9 @@ async function makeSession(
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* 建 records manifest(U5 fixture)。manifest
|
|
47
|
-
* sessionFile 指向 alive subagent session
|
|
48
|
-
* 从该路径提取 sessionId 建索引)。
|
|
46
|
+
* 建 records manifest(U5 fixture)。manifest 由终态 finalize 或 sync 批落标出口写入
|
|
47
|
+
* records/<sa-id>.json(创建时不写),sessionFile 指向 alive subagent session 的绝对路径
|
|
48
|
+
* (find 用 extractSessionIdFromFilename 从该路径提取 sessionId 建索引)。
|
|
49
49
|
*/
|
|
50
50
|
async function makeRecordManifest(
|
|
51
51
|
recordsDir: string,
|
|
@@ -242,7 +242,7 @@ describe('provideHashCandidates', () => {
|
|
|
242
242
|
await mkdir(otherCwdDir, { recursive: true })
|
|
243
243
|
})
|
|
244
244
|
afterEach(async () => {
|
|
245
|
-
await rm(agentDir, { recursive: true, force: true })
|
|
245
|
+
await rm(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
246
246
|
})
|
|
247
247
|
|
|
248
248
|
it('非 # 输入 → null(委托下家 provider)', async () => {
|
|
@@ -377,7 +377,7 @@ describe('createHashAutocompleteProvider - getSuggestions', () => {
|
|
|
377
377
|
await mkdir(cwdSessionDir, { recursive: true })
|
|
378
378
|
})
|
|
379
379
|
afterEach(async () => {
|
|
380
|
-
await rm(agentDir, { recursive: true, force: true })
|
|
380
|
+
await rm(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
381
381
|
})
|
|
382
382
|
|
|
383
383
|
it('signal.aborted → 早退返 null,不调 current、不做 IO', async () => {
|