@tobycaimf/dsh-archived-sessions 1.0.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,148 @@
1
+ // Regression tests for the session-log zstd frame rewrite.
2
+ //
3
+ // Background: moving a session between workspaces rewrites frame0's `cwd`.
4
+ // Locating frame boundaries by scanning for the 4-byte zstd magic alone
5
+ // produces false positives (the same bytes can occur inside compressed data),
6
+ // which corrupts frame0 and takes down the whole web profile on next start —
7
+ // the persistence layer requires frame0 to be exactly one session-header line.
8
+ //
9
+ // These tests pin both halves of the fix:
10
+ // 1. frame boundaries are validated by decompression
11
+ // 2. a non-session frame0 is rejected, never rewritten
12
+
13
+ import assert from 'node:assert/strict'
14
+ import { test } from 'node:test'
15
+ import zlib from 'node:zlib'
16
+ import {
17
+ ZSTD_MAGIC,
18
+ buildSessionLog,
19
+ findZstdFrameStarts,
20
+ readFrame0,
21
+ rewriteFrame0CwdInMemory,
22
+ } from '../src/zstd-frame.js'
23
+
24
+ // zstd magic bytes in file order (28 B5 2F FD) — the little-endian uint32
25
+ // 0xFD2FB528 is what readUInt32LE yields.
26
+ const MAGIC_BYTES = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
27
+
28
+ const sessionHeader = (cwd = '/workspaces/alpha') => ({
29
+ type: 'session',
30
+ id: 'sess-1',
31
+ cwd,
32
+ title: 'Test session',
33
+ createdAt: 1700000000000,
34
+ })
35
+
36
+ test('findZstdFrameStarts: locates every frame of a well-formed log', () => {
37
+ const log = buildSessionLog(sessionHeader(), [
38
+ { type: 'session/title', data: { title: 'Renamed' } },
39
+ { type: 'agent/inbox/spliced', seq: 4 },
40
+ { type: 'agent/message', seq: 5 },
41
+ ])
42
+ const starts = findZstdFrameStarts(log)
43
+ assert.equal(starts.length, 4, 'header + 3 event frames')
44
+ assert.equal(starts[0], 0, 'first frame starts at offset 0')
45
+ assert.deepEqual(starts, [...starts].sort((a, b) => a - b), 'offsets ascending')
46
+ })
47
+
48
+ test('findZstdFrameStarts: ignores a magic followed by undecodable data', () => {
49
+ // Regression: the old implementation accepted every 4-byte magic match, so a
50
+ // magic sequence occurring inside a frame's payload was treated as the start
51
+ // of the next frame — slicing frame0 wrong and corrupting the log.
52
+ const realFrame = zlib.zstdCompressSync(Buffer.from(JSON.stringify(sessionHeader()) + '\n'))
53
+ const buf = Buffer.concat([realFrame, Buffer.from('not a zstd frame at all'), MAGIC_BYTES, Buffer.from('tail')])
54
+
55
+ assert.deepEqual(
56
+ findZstdFrameStarts(buf),
57
+ [0],
58
+ 'only the real frame is recognised; the undecodable magic is filtered out',
59
+ )
60
+ })
61
+
62
+ test('findZstdFrameStarts: ignores a bare trailing magic', () => {
63
+ // Edge case: a 4-byte magic at the very end of the buffer does NOT throw when
64
+ // decompressed — Node returns an empty buffer. Accepting it would mark a
65
+ // phantom frame boundary, so empty output must be rejected too.
66
+ const realFrame = zlib.zstdCompressSync(Buffer.from(JSON.stringify(sessionHeader()) + '\n'))
67
+ const buf = Buffer.concat([realFrame, MAGIC_BYTES])
68
+ assert.equal(buf.length, realFrame.length + 4, 'bare magic sits at the very end')
69
+
70
+ assert.deepEqual(findZstdFrameStarts(buf), [0], 'the bare trailing magic is not treated as a frame')
71
+ })
72
+
73
+ test('findZstdFrameStarts: returns empty for a buffer with no decodable frame', () => {
74
+ const starts = findZstdFrameStarts(Buffer.concat([MAGIC_BYTES, Buffer.from('corrupt payload')]))
75
+ assert.deepEqual(starts, [])
76
+ })
77
+
78
+ test('rewriteFrame0Cwd: updates cwd and preserves every subsequent frame byte-for-byte', () => {
79
+ const events = [{ type: 'session/title', data: { title: 'Renamed' } }, { type: 'agent/message', seq: 9 }]
80
+ const log = buildSessionLog(sessionHeader('/workspaces/alpha'), events)
81
+
82
+ const next = rewriteFrame0CwdInMemory(log, '/workspaces/beta')
83
+
84
+ // frame0 reflects the new cwd and is still exactly one line
85
+ const { obj, lineCount } = readFrame0(next)
86
+ assert.equal(obj.type, 'session')
87
+ assert.equal(obj.cwd, '/workspaces/beta')
88
+ assert.equal(lineCount, 1, 'frame0 must stay a single header line')
89
+
90
+ // Header fields other than cwd are untouched
91
+ assert.equal(obj.id, 'sess-1')
92
+ assert.equal(obj.title, 'Test session')
93
+
94
+ // Everything after frame0 is preserved verbatim
95
+ const starts = findZstdFrameStarts(log)
96
+ const nextStarts = findZstdFrameStarts(next)
97
+ assert.equal(nextStarts.length, starts.length, 'frame count unchanged')
98
+ assert.deepEqual(
99
+ next.subarray(nextStarts[1]),
100
+ log.subarray(starts[1]),
101
+ 'trailing frames are byte-identical',
102
+ )
103
+ })
104
+
105
+ test('rewriteFrame0Cwd: rejects a corrupted frame0 instead of baking it in', () => {
106
+ // Regression: this is the exact shape of the file that broke `dsm web` —
107
+ // frame0 held an event record, not a session header. The old code would have
108
+ // parsed it, set `cwd`, and written it back as frame0, making the corruption
109
+ // permanent and unrecoverable.
110
+ const corrupted = buildSessionLog(
111
+ { type: 'agent/inbox/spliced', seq: 4 },
112
+ [{ type: 'session/title', data: { title: 'Orphaned' } }],
113
+ )
114
+
115
+ assert.throws(
116
+ () => rewriteFrame0CwdInMemory(corrupted, '/workspaces/beta'),
117
+ /帧0 不是 session header/,
118
+ 'must refuse to rewrite a non-session frame0',
119
+ )
120
+ })
121
+
122
+ test('rewriteFrame0Cwd: rejects a log with no decodable zstd frame', () => {
123
+ assert.throws(
124
+ () => rewriteFrame0CwdInMemory(Buffer.from('plain text, not zstd'), '/workspaces/beta'),
125
+ /无 zstd 帧/,
126
+ )
127
+ })
128
+
129
+ test('buildSessionLog + readFrame0: round-trip matches DSH persistence layout', () => {
130
+ const header = sessionHeader('/workspaces/gamma')
131
+ const log = buildSessionLog(header, [{ type: 'agent/message', seq: 2 }])
132
+
133
+ // The persistence layer's frame0 reader requires the first frame to decode to
134
+ // exactly one line terminated by a newline (assertZstdHeaderFrame).
135
+ const starts = findZstdFrameStarts(log)
136
+ const end0 = starts.length > 1 ? starts[1] : log.length
137
+ const plaintext = zlib.zstdDecompressSync(log.subarray(starts[0], end0)).toString('utf8')
138
+
139
+ assert.equal(plaintext.indexOf('\n'), plaintext.length - 1, 'frame0 is one line + trailing newline')
140
+ const { obj } = readFrame0(log)
141
+ assert.deepEqual(obj, header)
142
+ })
143
+
144
+ test('ZSTD_MAGIC constant matches the on-disk byte order', () => {
145
+ const buf = Buffer.alloc(4)
146
+ buf.writeUInt32LE(ZSTD_MAGIC, 0)
147
+ assert.deepEqual(buf, MAGIC_BYTES)
148
+ })