@mzzsfy/dsh-usage-dash 0.2.0 → 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.
@@ -0,0 +1,365 @@
1
+ // archive-reader 行为测试:宿主 sessionPersistence API 多版本适配层的
2
+ // 工厂分派与各代映射。宿主 API 形状以各代真实签名为蓝本,collector 只见
3
+ // 内部统一契约(list → [{id}],readLog → {inheritedEventCount, events})。
4
+ // 分页读、close 吞错、畸形行丢弃对齐竞品 0.1.12 双路径语义;
5
+ // V3 日志兼容由 collector 测试的专有事件忽略用例钉住。
6
+
7
+ import test from 'node:test'
8
+ import assert from 'node:assert/strict'
9
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
10
+ import { tmpdir } from 'node:os'
11
+ import { join } from 'node:path'
12
+ import { zstdCompressSync } from 'node:zlib'
13
+
14
+ import { createArchiveReader } from '../src/archive-reader.js'
15
+
16
+ const ev = (seq) => ({ type: 'turn/end', seq, time: 0, data: { turn: 0 } })
17
+ const NAMES = {
18
+ handle: 'HandleArchiveReader',
19
+ inspect: 'InspectArchiveReader',
20
+ unknown: 'UnknownArchiveReader',
21
+ }
22
+ const nameOf = (reader) => reader.constructor.name
23
+
24
+ test('工厂:open 形宿主分派到 handle 代适配器', () => {
25
+ const persistence = { open: () => {}, list: () => {} }
26
+ assert.equal(nameOf(createArchiveReader(persistence)), NAMES.handle)
27
+ })
28
+
29
+ test('工厂:inspect 形宿主分派到 inspect 代适配器', () => {
30
+ const persistence = { inspect: () => {}, list: () => {} }
31
+ assert.equal(nameOf(createArchiveReader(persistence)), NAMES.inspect)
32
+ })
33
+
34
+ test('工厂:两代形状都不匹配分派到 unknown 恒抛适配器', async () => {
35
+ const reader = createArchiveReader({})
36
+ assert.equal(nameOf(reader), NAMES.unknown)
37
+ await assert.rejects(reader.list(), /sessionPersistence/)
38
+ await assert.rejects(reader.readLog('s1'), /sessionPersistence/)
39
+ })
40
+
41
+ test('handle 代:list 归一 snapshot 为 {id},signal 包裹透传,畸形行丢弃', () => {
42
+ return withDirectRoot(async () => {
43
+ const seen = []
44
+ const persistence = {
45
+ async list(options) {
46
+ seen.push(options)
47
+ return [
48
+ { header: { id: 'a' }, revision: 'r1', sizeBytes: 10 },
49
+ { header: { id: 'b' }, revision: 'r2' },
50
+ { revision: 'no-header' },
51
+ null,
52
+ { header: { id: '' } },
53
+ ]
54
+ },
55
+ async open() {
56
+ throw new Error('not reached')
57
+ },
58
+ }
59
+ const signal = new AbortController().signal
60
+ const reader = createArchiveReader(persistence)
61
+ assert.deepEqual(await reader.list(signal), [{ id: 'a' }, { id: 'b' }])
62
+ assert.deepEqual(seen, [{ signal }])
63
+ })
64
+ })
65
+
66
+ test('handle 代:readLog 分页循环直到空页,handle 元数据透出', async () => {
67
+ const calls = []
68
+ const events = Array.from({ length: 1250 }, (_, seq) => ev(seq))
69
+ const persistence = {
70
+ async open(id, access, options) {
71
+ calls.push(['open', id, access, options])
72
+ return {
73
+ header: { id },
74
+ inheritedEventCount: 3,
75
+ async read(offset, length, readOptions) {
76
+ calls.push(['read', offset, length])
77
+ assert.equal(typeof readOptions, 'object')
78
+ assert.equal(length, 500)
79
+ return { eventState: 'detached', events: events.slice(offset, offset + length) }
80
+ },
81
+ async close() {
82
+ calls.push(['close'])
83
+ },
84
+ }
85
+ },
86
+ }
87
+ const signal = new AbortController().signal
88
+ const reader = createArchiveReader(persistence)
89
+ const result = await reader.readLog('s1', signal)
90
+ assert.equal(result.inheritedEventCount, 3)
91
+ assert.deepEqual(result.events, events)
92
+ const reads = calls.filter(([kind]) => kind === 'read')
93
+ // 1250 条 = 500+500+250 三页取到,第 4 次读到空页才终止
94
+ assert.equal(reads.length, 4)
95
+ assert.deepEqual(reads.map(([, offset]) => offset), [0, 500, 1000, 1250])
96
+ assert.deepEqual(calls[calls.length - 1], ['close'])
97
+ })
98
+
99
+ test('handle 代:inheritedEventCount 非法值回落 0', async () => {
100
+ const persistence = {
101
+ async open() {
102
+ return {
103
+ inheritedEventCount: -5,
104
+ async read() {
105
+ return { eventState: 'detached', events: [] }
106
+ },
107
+ async close() {},
108
+ }
109
+ },
110
+ }
111
+ const reader = createArchiveReader(persistence)
112
+ const result = await reader.readLog('s1')
113
+ assert.equal(result.inheritedEventCount, 0)
114
+ assert.deepEqual(result.events, [])
115
+ })
116
+
117
+ test('handle 代:read 抛错时 close 仍被调用且原错误传播', async () => {
118
+ let closed = 0
119
+ const failure = new Error('corrupted log')
120
+ const persistence = {
121
+ async open() {
122
+ return {
123
+ inheritedEventCount: 0,
124
+ async read() {
125
+ throw failure
126
+ },
127
+ async close() {
128
+ closed += 1
129
+ },
130
+ }
131
+ },
132
+ }
133
+ const reader = createArchiveReader(persistence)
134
+ await assert.rejects(reader.readLog('s1'), (error) => error === failure)
135
+ assert.equal(closed, 1)
136
+ })
137
+
138
+ test('handle 代:read 抛错且 close 也抛错时,原错误传播不被掩盖', async () => {
139
+ const failure = new Error('corrupted log')
140
+ const persistence = {
141
+ async open() {
142
+ return {
143
+ inheritedEventCount: 0,
144
+ async read() {
145
+ throw failure
146
+ },
147
+ async close() {
148
+ throw new Error('close failure')
149
+ },
150
+ }
151
+ },
152
+ }
153
+ const reader = createArchiveReader(persistence)
154
+ await assert.rejects(reader.readLog('s1'), (error) => error === failure)
155
+ })
156
+
157
+ test('handle 代:read 成功后 close 失败被吞掉,读取结果照常返回', async () => {
158
+ let closed = 0
159
+ const events = [ev(0)]
160
+ let reads = 0
161
+ const persistence = {
162
+ async open() {
163
+ return {
164
+ inheritedEventCount: 0,
165
+ async read() {
166
+ reads += 1
167
+ // 首读返回数据页,再读返回空页终止分页循环
168
+ return { eventState: 'detached', events: reads === 1 ? events : [] }
169
+ },
170
+ async close() {
171
+ closed += 1
172
+ throw new Error('close failure')
173
+ },
174
+ }
175
+ },
176
+ }
177
+ const reader = createArchiveReader(persistence)
178
+ const result = await reader.readLog('s1')
179
+ assert.equal(closed, 1)
180
+ assert.deepEqual(result, { inheritedEventCount: 0, events })
181
+ })
182
+
183
+ test('handle 代:abort 中断分页循环,当前页已读数据保留', async () => {
184
+ const controller = new AbortController()
185
+ const events = Array.from({ length: 700 }, (_, seq) => ev(seq))
186
+ const persistence = {
187
+ async open() {
188
+ return {
189
+ inheritedEventCount: 0,
190
+ async read(offset) {
191
+ if (offset > 0) controller.abort()
192
+ return { eventState: 'detached', events: events.slice(offset, offset + 500) }
193
+ },
194
+ async close() {},
195
+ }
196
+ },
197
+ }
198
+ const reader = createArchiveReader(persistence)
199
+ // abort 在第二页读取时触发:第二页已返回故被保留,下一轮循环检测到中止退出
200
+ const result = await reader.readLog('s1', controller.signal)
201
+ assert.equal(result.events.length, 700)
202
+ })
203
+
204
+ test('handle 代:open 抛错直接传播,不触发 close', async () => {
205
+ const failure = new Error('format unsupported')
206
+ const persistence = {
207
+ async open() {
208
+ throw failure
209
+ },
210
+ }
211
+ const reader = createArchiveReader(persistence)
212
+ await assert.rejects(reader.readLog('s1'), (error) => error === failure)
213
+ })
214
+
215
+ test('inspect 代:list 归一 header 数组为 {id},signal 直传,畸形行丢弃', () => {
216
+ return withDirectRoot(async () => {
217
+ const seen = []
218
+ const persistence = {
219
+ async list(arg) {
220
+ seen.push(arg)
221
+ return [{ id: 'a', createdAt: 1 }, { id: 'b', createdAt: 2 }, null, {}]
222
+ },
223
+ async inspect() {
224
+ throw new Error('not reached')
225
+ },
226
+ }
227
+ const signal = new AbortController().signal
228
+ const reader = createArchiveReader(persistence)
229
+ assert.deepEqual(await reader.list(signal), [{ id: 'a' }, { id: 'b' }])
230
+ assert.deepEqual(seen, [signal])
231
+ })
232
+ })
233
+
234
+ test('inspect 代:readLog 经 inspect 整读,signal 直传', async () => {
235
+ const seen = []
236
+ const events = [ev(0)]
237
+ const signal = new AbortController().signal
238
+ const persistence = {
239
+ async inspect(id, arg) {
240
+ seen.push([id, arg])
241
+ return { meta: { id }, inheritedEventCount: 2, events }
242
+ },
243
+ }
244
+ const reader = createArchiveReader(persistence)
245
+ assert.deepEqual(
246
+ await reader.readLog('s1', signal),
247
+ { inheritedEventCount: 2, events },
248
+ )
249
+ assert.deepEqual(seen, [['s1', signal]])
250
+ })
251
+
252
+ // 降级直读的测试桩:DSH_HOME 指向临时根,档案落 sessions/<bucket>/<id>/
253
+ function withDirectRoot(run) {
254
+ const root = mkdtempSync(join(tmpdir(), 'ud-fallback-'))
255
+ process.env.DSH_HOME = root
256
+ return Promise.resolve(run(root)).finally(() => {
257
+ delete process.env.DSH_HOME
258
+ rmSync(root, { recursive: true, force: true })
259
+ })
260
+ }
261
+
262
+ function seedArtifact(root, id, events) {
263
+ const text = [
264
+ JSON.stringify({ type: 'session/header', id }),
265
+ ...events.map((event) => JSON.stringify(event)),
266
+ ].join('\n')
267
+ const dir = join(root, 'sessions', 'bucket-x', id)
268
+ mkdirSync(dir, { recursive: true })
269
+ writeFileSync(join(dir, 'session.v3.jsonl.zstd'), zstdCompressSync(Buffer.from(text)))
270
+ }
271
+
272
+ test('handle 代:宿主拒读(descriptor)降级文件直读', () => {
273
+ return withDirectRoot(async (root) => {
274
+ seedArtifact(root, 'refused', [ev(0), ev(1)])
275
+ const persistence = {
276
+ async open() {
277
+ return {
278
+ inheritedEventCount: 0,
279
+ async read() {
280
+ throw new Error('subagent session "refused" uses unsupported descriptor version 2')
281
+ },
282
+ async close() {},
283
+ }
284
+ },
285
+ }
286
+ const reader = createArchiveReader(persistence)
287
+ const result = await reader.readLog('refused')
288
+ assert.equal(result.inheritedEventCount, 0)
289
+ assert.equal(result.events.length, 2)
290
+ })
291
+ })
292
+
293
+ test('handle 代:open 阶段拒读(descriptor 校验)同样降级直读', () => {
294
+ return withDirectRoot(async (root) => {
295
+ seedArtifact(root, 'open-refused', [ev(3)])
296
+ const persistence = {
297
+ async open() {
298
+ throw new Error('session "open-refused" header uses unsupported descriptor version 2')
299
+ },
300
+ }
301
+ const reader = createArchiveReader(persistence)
302
+ const result = await reader.readLog('open-refused')
303
+ assert.equal(result.events.length, 1)
304
+ })
305
+ })
306
+
307
+ test('handle 代:open 拒读且直读失败时,原错误传播', () => {
308
+ return withDirectRoot(async () => {
309
+ const persistence = {
310
+ async open() {
311
+ throw new Error('session "absent" header uses unsupported descriptor version 2')
312
+ },
313
+ }
314
+ const reader = createArchiveReader(persistence)
315
+ await assert.rejects(reader.readLog('absent'), /descriptor version 2/)
316
+ })
317
+ })
318
+
319
+ test('handle 代:非拒读错误不触发直读,原错误传播', () => {
320
+ return withDirectRoot((root) => {
321
+ seedArtifact(root, 'gone', [ev(0)])
322
+ const persistence = {
323
+ async open() {
324
+ throw new Error('session "gone" not found')
325
+ },
326
+ }
327
+ const reader = createArchiveReader(persistence)
328
+ return assert.rejects(reader.readLog('gone'), /not found/)
329
+ })
330
+ })
331
+
332
+ test('handle 代:拒读且直读也失败时,原错误传播', () => {
333
+ return withDirectRoot(async (root) => {
334
+ const persistence = {
335
+ async open() {
336
+ return {
337
+ inheritedEventCount: 0,
338
+ async read() {
339
+ throw new Error('stored log is corrupt: SessionFormatError: seq gap')
340
+ },
341
+ async close() {},
342
+ }
343
+ },
344
+ }
345
+ const reader = createArchiveReader(persistence)
346
+ await assert.rejects(reader.readLog('absent'), /corrupt/)
347
+ })
348
+ })
349
+
350
+ test('handle 代:list 并集补齐宿主不可见的磁盘档案', () => {
351
+ return withDirectRoot(async (root) => {
352
+ seedArtifact(root, 'v3-only', [ev(0)])
353
+ const persistence = {
354
+ async list() {
355
+ return [{ header: { id: 'host-known' } }]
356
+ },
357
+ async open() {
358
+ throw new Error('not reached')
359
+ },
360
+ }
361
+ const reader = createArchiveReader(persistence)
362
+ const ids = (await reader.list()).map((row) => row.id).sort()
363
+ assert.deepEqual(ids, ['host-known', 'v3-only'])
364
+ })
365
+ })