socketduct 0.1.24 → 0.1.26

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.
Files changed (60) hide show
  1. package/AGENTS.md +2 -2
  2. package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
  3. package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
  4. package/package.json +2 -2
  5. package/scripts/test.js +9 -14
  6. package/src/client/multiplexed-transport-set.js +20 -2
  7. package/src/logging/compressed-log-writer.js +1 -1
  8. package/src/logging/zstd-frame-codec.js +15 -1
  9. package/src/reverse/client.js +44 -1
  10. package/src/reverse/hub.js +10 -1
  11. package/src/reverse/runtime.js +11 -2
  12. package/test/e2e/gateway-process.test.js +93 -92
  13. package/test/e2e/logger-shutdown-process.test.js +40 -37
  14. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  15. package/test/e2e/reverse-gateway-process.test.js +328 -318
  16. package/test/integration/initial-target-recovery.test.js +333 -334
  17. package/test/integration/logging-cli.test.js +121 -119
  18. package/test/integration/logging-compressed-recovery.test.js +96 -95
  19. package/test/integration/multiplexed-compatibility.test.js +178 -177
  20. package/test/integration/multiplexed-corrections.test.js +360 -359
  21. package/test/integration/multiplexed-failures.test.js +253 -251
  22. package/test/integration/multiplexed-transport-set.test.js +334 -322
  23. package/test/integration/multiplexed-transport.test.js +135 -134
  24. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  25. package/test/integration/reverse-gateway.test.js +2309 -2218
  26. package/test/integration/reverse-resume-continuity.test.js +219 -218
  27. package/test/integration/reverse-route-class.test.js +363 -278
  28. package/test/integration/socketduct-http-agent.test.js +314 -312
  29. package/test/integration/socketduct-https-agent.test.js +516 -485
  30. package/test/integration/stream-compression.test.js +862 -824
  31. package/test/integration/tcp-forwarding.test.js +2550 -2447
  32. package/test/integration/tls-forwarding.test.js +593 -592
  33. package/test/unit/auth/auth.test.js +43 -44
  34. package/test/unit/auth/static-token-store.test.js +9 -8
  35. package/test/unit/client/connect.test.js +41 -40
  36. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  37. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  38. package/test/unit/client/multiplexed-transport-set.test.js +845 -732
  39. package/test/unit/client/socketduct-socket.test.js +768 -771
  40. package/test/unit/client/virtual-socket.test.js +198 -168
  41. package/test/unit/config/docker-gateway-files.test.js +224 -224
  42. package/test/unit/config/environment.test.js +472 -492
  43. package/test/unit/index.test.js +53 -52
  44. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  45. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  46. package/test/unit/logging/log-retention.test.js +52 -47
  47. package/test/unit/logging/log-segment-index.test.js +84 -81
  48. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  49. package/test/unit/protocol/compression.test.js +254 -298
  50. package/test/unit/protocol/frame.test.js +280 -276
  51. package/test/unit/protocol/v3-compression.test.js +21 -23
  52. package/test/unit/protocol/v3-frame.test.js +42 -41
  53. package/test/unit/release-configuration.test.js +15 -12
  54. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  55. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  56. package/test/unit/server/control-handler.test.js +232 -231
  57. package/test/unit/server/primary.test.js +51 -49
  58. package/test/unit/server/relay-runtime.test.js +280 -294
  59. package/test/unit/sessions/session.test.js +48 -62
  60. package/test/unit/spool/spool-file.test.js +401 -385
@@ -1,336 +1,340 @@
1
- import assert from "node:assert/strict"
2
- import test from "node:test"
1
+ import {describe, expect, it} from "@velocious/testing"
3
2
 
4
3
  import {FrameDecoder, encodeCompactAck, encodeCompactData, encodeFrame} from "../../../src/protocol/frame.js"
5
4
 
6
- test("encodeFrame and FrameDecoder preserve header fields and payload bytes", () => {
7
- const payload = Buffer.from([0, 1, 2, 253, 254, 255])
8
- const encoded = encodeFrame({type: "stream.data", sessionId: "session-1", sequence: 42}, payload)
9
-
10
- const decoder = new FrameDecoder()
11
- const frames = decoder.push(encoded)
5
+ describe("frame protocol", () => {
6
+ it("encodeFrame and FrameDecoder preserve header fields and payload bytes", () => {
7
+ const payload = Buffer.from([0, 1, 2, 253, 254, 255])
8
+ const encoded = encodeFrame({type: "stream.data", sessionId: "session-1", sequence: 42}, payload)
9
+
10
+ const decoder = new FrameDecoder()
11
+ const frames = decoder.push(encoded)
12
+
13
+ expect(frames.length).toBe(1)
14
+ expect(frames[0]?.header).toEqual({
15
+ type: "stream.data",
16
+ sessionId: "session-1",
17
+ sequence: 42
18
+ })
19
+ if (!frames[0]) throw new Error("expected a decoded stream.data frame")
20
+ expect(Buffer.from(frames[0].payload).equals(Buffer.from(payload))).toBe(true)
21
+ expect(frames[0]?.wireFormat).toBe("envelope")
22
+ })
12
23
 
13
- assert.equal(frames.length, 1)
14
- assert.deepEqual(frames[0]?.header, {
15
- type: "stream.data",
16
- sessionId: "session-1",
17
- sequence: 42
24
+ it("FrameDecoder decodes frames split across multiple TCP chunks", () => {
25
+ const first = encodeFrame({type: "auth.request", requestId: "a"}, Buffer.from("one"))
26
+ const second = encodeFrame({type: "session.create", requestId: "b"}, Buffer.from("two"))
27
+ const combined = Buffer.concat([first, second])
28
+ const decoder = new FrameDecoder()
29
+
30
+ expect(decoder.push(combined.subarray(0, 2))).toEqual([])
31
+ expect(decoder.push(combined.subarray(2, 9))).toEqual([])
32
+ const frames = decoder.push(combined.subarray(9))
33
+
34
+ expect(frames.length).toBe(2)
35
+ expect(frames[0]?.header.type).toBe("auth.request")
36
+ expect(frames[0]?.payload.toString("utf8")).toBe("one")
37
+ expect(frames[1]?.header.type).toBe("session.create")
38
+ expect(frames[1]?.payload.toString("utf8")).toBe("two")
18
39
  })
19
- assert.deepEqual(frames[0]?.payload, payload)
20
- assert.equal(frames[0]?.wireFormat, "envelope")
21
- })
22
40
 
23
- test("FrameDecoder decodes frames split across multiple TCP chunks", () => {
24
- const first = encodeFrame({type: "auth.request", requestId: "a"}, Buffer.from("one"))
25
- const second = encodeFrame({type: "session.create", requestId: "b"}, Buffer.from("two"))
26
- const combined = Buffer.concat([first, second])
27
- const decoder = new FrameDecoder()
28
-
29
- assert.deepEqual(decoder.push(combined.subarray(0, 2)), [])
30
- assert.deepEqual(decoder.push(combined.subarray(2, 9)), [])
31
- const frames = decoder.push(combined.subarray(9))
32
-
33
- assert.equal(frames.length, 2)
34
- assert.equal(frames[0]?.header.type, "auth.request")
35
- assert.equal(frames[0]?.payload.toString("utf8"), "one")
36
- assert.equal(frames[1]?.header.type, "session.create")
37
- assert.equal(frames[1]?.payload.toString("utf8"), "two")
38
- })
41
+ it("FrameDecoder rejects invalid header lengths before allocating payload buffers", () => {
42
+ const decoder = new FrameDecoder({maxHeaderBytes: 8})
43
+ const invalid = Buffer.alloc(4)
44
+ invalid.writeUInt32BE(9, 0)
39
45
 
40
- test("FrameDecoder rejects invalid header lengths before allocating payload buffers", () => {
41
- const decoder = new FrameDecoder({maxHeaderBytes: 8})
42
- const invalid = Buffer.alloc(4)
43
- invalid.writeUInt32BE(9, 0)
46
+ expect(() => decoder.push(invalid)).toThrow(/header length/i)
47
+ })
44
48
 
45
- assert.throws(() => decoder.push(invalid), /header length/i)
46
- })
49
+ it("FrameDecoder rejects payloads larger than the configured limit", () => {
50
+ const decoder = new FrameDecoder({maxPayloadBytes: 2})
51
+ const encoded = encodeFrame({type: "stream.data"}, Buffer.from("too-large"))
47
52
 
48
- test("FrameDecoder rejects payloads larger than the configured limit", () => {
49
- const decoder = new FrameDecoder({maxPayloadBytes: 2})
50
- const encoded = encodeFrame({type: "stream.data"}, Buffer.from("too-large"))
53
+ expect(() => decoder.push(encoded)).toThrow(/payload length/i)
54
+ })
51
55
 
52
- assert.throws(() => decoder.push(encoded), /payload length/i)
53
- })
56
+ it("FrameDecoder handles empty chunk without error", () => {
57
+ const decoder = new FrameDecoder()
58
+ const frames = decoder.push(Buffer.alloc(0))
59
+ expect(frames).toEqual([])
60
+ })
54
61
 
55
- test("FrameDecoder handles empty chunk without error", () => {
56
- const decoder = new FrameDecoder()
57
- const frames = decoder.push(Buffer.alloc(0))
58
- assert.deepEqual(frames, [])
59
- })
62
+ it("compact v2 uncompressed data and ACK records round-trip beside envelope controls", () => {
63
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
64
+ const frames = decoder.push(Buffer.concat([
65
+ encodeFrame({type: "session.resumed", identifier: "s1", framing: 2}),
66
+ encodeCompactData({sequence: 300, payload: Buffer.from("raw")}),
67
+ encodeCompactAck(257),
68
+ encodeFrame({type: "session.close", direction: "target-to-client"})
69
+ ]))
70
+ expect(frames.map(({header, payload}) => ({header, payload: payload.toString()}))).toEqual([
71
+ {header: {type: "session.resumed", identifier: "s1", framing: 2}, payload: ""},
72
+ {header: {type: "stream.data", direction: "target-to-client", sequence: 300, compact: true}, payload: "raw"},
73
+ {header: {type: "stream.ack", direction: "client-to-target", ackSequence: 257, compact: true}, payload: ""},
74
+ {header: {type: "session.close", direction: "target-to-client"}, payload: ""}
75
+ ])
76
+ })
60
77
 
61
- test("compact v2 uncompressed data and ACK records round-trip beside envelope controls", () => {
62
- const decoder = new FrameDecoder({compactDirection: "target-to-client"})
63
- const frames = decoder.push(Buffer.concat([
64
- encodeFrame({type: "session.resumed", identifier: "s1", framing: 2}),
65
- encodeCompactData({sequence: 300, payload: Buffer.from("raw")}),
66
- encodeCompactAck(257),
67
- encodeFrame({type: "session.close", direction: "target-to-client"})
68
- ]))
69
- assert.deepEqual(frames.map(({header, payload}) => ({header, payload: payload.toString()})), [
70
- {header: {type: "session.resumed", identifier: "s1", framing: 2}, payload: ""},
71
- {header: {type: "stream.data", direction: "target-to-client", sequence: 300, compact: true}, payload: "raw"},
72
- {header: {type: "stream.ack", direction: "client-to-target", ackSequence: 257, compact: true}, payload: ""},
73
- {header: {type: "session.close", direction: "target-to-client"}, payload: ""}
74
- ])
75
- })
78
+ it("compact v2 zstd records normalize compression metadata", () => {
79
+ const decoder = new FrameDecoder({compactDirection: "client-to-target"})
80
+ const [frame] = decoder.push(encodeCompactData({sequence: Number.MAX_SAFE_INTEGER, dataLength: 4096, payload: Buffer.from([1, 2, 3])}))
81
+ expect(frame?.header).toEqual({type: "stream.data", direction: "client-to-target", sequence: Number.MAX_SAFE_INTEGER, compact: true, encoding: "zstd", dataLength: 4096})
82
+ if (!frame) throw new Error("expected a decoded compact frame")
83
+ expect(Buffer.from(frame.payload).equals(Buffer.from([1, 2, 3]))).toBe(true)
84
+ expect(frame?.wireFormat).toBe("compact")
85
+ })
76
86
 
77
- test("compact v2 zstd records normalize compression metadata", () => {
78
- const decoder = new FrameDecoder({compactDirection: "client-to-target"})
79
- const [frame] = decoder.push(encodeCompactData({sequence: Number.MAX_SAFE_INTEGER, dataLength: 4096, payload: Buffer.from([1, 2, 3])}))
80
- assert.deepEqual(frame?.header, {type: "stream.data", direction: "client-to-target", sequence: Number.MAX_SAFE_INTEGER, compact: true, encoding: "zstd", dataLength: 4096})
81
- assert.deepEqual(frame?.payload, Buffer.from([1, 2, 3]))
82
- assert.equal(frame?.wireFormat, "compact")
83
- })
87
+ it("compact decoder waits for fragmented records", () => {
88
+ const encoded = encodeCompactData({sequence: 129, payload: Buffer.from("fragmented")})
89
+ const decoder = new FrameDecoder({compactDirection: "client-to-target"})
90
+ expect(decoder.push(encoded.subarray(0, 3))).toEqual([])
91
+ expect(decoder.push(encoded.subarray(3))[0]?.payload.toString()).toBe("fragmented")
92
+ })
84
93
 
85
- test("compact decoder waits for fragmented records", () => {
86
- const encoded = encodeCompactData({sequence: 129, payload: Buffer.from("fragmented")})
87
- const decoder = new FrameDecoder({compactDirection: "client-to-target"})
88
- assert.deepEqual(decoder.push(encoded.subarray(0, 3)), [])
89
- assert.equal(decoder.push(encoded.subarray(3))[0]?.payload.toString(), "fragmented")
90
- })
94
+ it("compact decoder rejects non-canonical, overflowing, and oversized varints", () => {
95
+ const options = /** @type {const} */ ({compactDirection: "client-to-target"})
96
+ expect(() => new FrameDecoder(options).push(Buffer.from([0x82, 0x80, 0x00]))).toThrow(/canonical/)
97
+ expect(() => new FrameDecoder(options).push(Buffer.from([0x82, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f]))).toThrow(/safe integer/)
98
+ expect(() => new FrameDecoder({...options, maxPayloadBytes: 2}).push(encodeCompactData({sequence: 3, payload: Buffer.from("abc")}))).toThrow(/payload length/)
99
+ })
91
100
 
92
- test("compact decoder rejects non-canonical, overflowing, and oversized varints", () => {
93
- const options = /** @type {const} */ ({compactDirection: "client-to-target"})
94
- assert.throws(() => new FrameDecoder(options).push(Buffer.from([0x82, 0x80, 0x00])), /canonical/)
95
- assert.throws(() => new FrameDecoder(options).push(Buffer.from([0x82, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f])), /safe integer/)
96
- assert.throws(() => new FrameDecoder({...options, maxPayloadBytes: 2}).push(encodeCompactData({sequence: 3, payload: Buffer.from("abc")})), /payload length/)
97
- })
101
+ it("FrameDecoder handles partial uint32 header length", () => {
102
+ const decoder = new FrameDecoder()
103
+ const partial = Buffer.alloc(2)
104
+ partial.writeUInt16BE(42, 0)
98
105
 
99
- test("FrameDecoder handles partial uint32 header length", () => {
100
- const decoder = new FrameDecoder()
101
- const partial = Buffer.alloc(2)
102
- partial.writeUInt16BE(42, 0)
106
+ expect(decoder.push(partial)).toEqual([])
107
+ })
103
108
 
104
- assert.deepEqual(decoder.push(partial), [])
105
- })
109
+ it("FrameDecoder handles partial header bytes after header length", () => {
110
+ const decoder = new FrameDecoder()
111
+ const buf = Buffer.alloc(6)
112
+ buf.writeUInt32BE(10, 0)
106
113
 
107
- test("FrameDecoder handles partial header bytes after header length", () => {
108
- const decoder = new FrameDecoder()
109
- const buf = Buffer.alloc(6)
110
- buf.writeUInt32BE(10, 0)
114
+ expect(decoder.push(buf)).toEqual([])
115
+ })
111
116
 
112
- assert.deepEqual(decoder.push(buf), [])
113
- })
117
+ it("FrameDecoder handles partial payload length uint32 after header", () => {
118
+ const header = JSON.stringify({type: "test"})
119
+ const buf = Buffer.alloc(4 + header.length + 2)
120
+ buf.writeUInt32BE(header.length, 0)
121
+ buf.write(header, 4, "utf8")
114
122
 
115
- test("FrameDecoder handles partial payload length uint32 after header", () => {
116
- const header = JSON.stringify({type: "test"})
117
- const buf = Buffer.alloc(4 + header.length + 2)
118
- buf.writeUInt32BE(header.length, 0)
119
- buf.write(header, 4, "utf8")
123
+ const decoder = new FrameDecoder()
124
+ expect(decoder.push(buf)).toEqual([])
125
+ })
120
126
 
121
- const decoder = new FrameDecoder()
122
- assert.deepEqual(decoder.push(buf), [])
123
- })
127
+ it("FrameDecoder handles partial payload bytes after full header and payload length", () => {
128
+ const header = JSON.stringify({type: "test"})
129
+ const buf = Buffer.alloc(4 + header.length + 4 + 3)
130
+ buf.writeUInt32BE(header.length, 0)
131
+ buf.write(header, 4, "utf8")
132
+ buf.writeUInt32BE(10, 4 + header.length)
124
133
 
125
- test("FrameDecoder handles partial payload bytes after full header and payload length", () => {
126
- const header = JSON.stringify({type: "test"})
127
- const buf = Buffer.alloc(4 + header.length + 4 + 3)
128
- buf.writeUInt32BE(header.length, 0)
129
- buf.write(header, 4, "utf8")
130
- buf.writeUInt32BE(10, 4 + header.length)
134
+ const decoder = new FrameDecoder()
135
+ expect(decoder.push(buf)).toEqual([])
136
+ })
131
137
 
132
- const decoder = new FrameDecoder()
133
- assert.deepEqual(decoder.push(buf), [])
134
- })
138
+ it("FrameDecoder decodes one frame from a chunk that arrives byte-by-byte (uint32 split)", () => {
139
+ const payload = Buffer.from("hello")
140
+ const encoded = encodeFrame({type: "ping"}, payload)
141
+ const decoder = new FrameDecoder()
135
142
 
136
- test("FrameDecoder decodes one frame from a chunk that arrives byte-by-byte (uint32 split)", () => {
137
- const payload = Buffer.from("hello")
138
- const encoded = encodeFrame({type: "ping"}, payload)
139
- const decoder = new FrameDecoder()
140
-
141
- for (let i = 0; i < encoded.length; i++) {
142
- const result = decoder.push(encoded.subarray(i, i + 1))
143
- if (i < encoded.length - 1) {
144
- assert.deepEqual(result, [])
145
- } else {
146
- assert.equal(result.length, 1)
147
- assert.equal(result[0]?.header.type, "ping")
148
- assert.deepEqual(result[0]?.payload, payload)
143
+ for (let i = 0; i < encoded.length; i++) {
144
+ const result = decoder.push(encoded.subarray(i, i + 1))
145
+ if (i < encoded.length - 1) {
146
+ expect(result).toEqual([])
147
+ } else {
148
+ expect(result.length).toBe(1)
149
+ expect(result[0]?.header.type).toBe("ping")
150
+ if (!result[0]) throw new Error("expected a decoded ping frame")
151
+ expect(Buffer.from(result[0].payload).equals(Buffer.from(payload))).toBe(true)
152
+ }
149
153
  }
150
- }
151
- })
154
+ })
152
155
 
153
- test("FrameDecoder decodes multiple complete frames from a single push", () => {
154
- const frame1 = encodeFrame({type: "alpha"}, Buffer.from("a"))
155
- const frame2 = encodeFrame({type: "beta"}, Buffer.from("bb"))
156
- const frame3 = encodeFrame({type: "gamma"}, Buffer.from("ccc"))
157
- const combined = Buffer.concat([frame1, frame2, frame3])
158
-
159
- const decoder = new FrameDecoder()
160
- const frames = decoder.push(combined)
161
-
162
- assert.equal(frames.length, 3)
163
- assert.equal(frames[0]?.header.type, "alpha")
164
- assert.equal(frames[0]?.payload.toString(), "a")
165
- assert.equal(frames[1]?.header.type, "beta")
166
- assert.equal(frames[1]?.payload.toString(), "bb")
167
- assert.equal(frames[2]?.header.type, "gamma")
168
- assert.equal(frames[2]?.payload.toString(), "ccc")
169
- })
156
+ it("FrameDecoder decodes multiple complete frames from a single push", () => {
157
+ const frame1 = encodeFrame({type: "alpha"}, Buffer.from("a"))
158
+ const frame2 = encodeFrame({type: "beta"}, Buffer.from("bb"))
159
+ const frame3 = encodeFrame({type: "gamma"}, Buffer.from("ccc"))
160
+ const combined = Buffer.concat([frame1, frame2, frame3])
161
+
162
+ const decoder = new FrameDecoder()
163
+ const frames = decoder.push(combined)
164
+
165
+ expect(frames.length).toBe(3)
166
+ expect(frames[0]?.header.type).toBe("alpha")
167
+ expect(frames[0]?.payload.toString()).toBe("a")
168
+ expect(frames[1]?.header.type).toBe("beta")
169
+ expect(frames[1]?.payload.toString()).toBe("bb")
170
+ expect(frames[2]?.header.type).toBe("gamma")
171
+ expect(frames[2]?.payload.toString()).toBe("ccc")
172
+ })
170
173
 
171
- test("FrameDecoder with maxInputBytes rejects over-budget input", () => {
172
- const decoder = new FrameDecoder({maxInputBytes: 10})
173
- const frame = encodeFrame({type: "test"}, Buffer.from("hello"))
174
+ it("FrameDecoder with maxInputBytes rejects over-budget input", () => {
175
+ const decoder = new FrameDecoder({maxInputBytes: 10})
176
+ const frame = encodeFrame({type: "test"}, Buffer.from("hello"))
174
177
 
175
- assert.throws(() => decoder.push(frame), /input byte budget/i)
176
- })
178
+ expect(() => decoder.push(frame)).toThrow(/input byte budget/i)
179
+ })
177
180
 
178
- test("FrameDecoder with maxInputBytes allows input within budget and rejects on overflow", () => {
179
- const decoder = new FrameDecoder({maxInputBytes: 100})
181
+ it("FrameDecoder with maxInputBytes allows input within budget and rejects on overflow", () => {
182
+ const decoder = new FrameDecoder({maxInputBytes: 100})
180
183
 
181
- const header = JSON.stringify({type: "small"})
182
- const smallFrame = Buffer.alloc(4 + header.length + 4 + 5)
183
- smallFrame.writeUInt32BE(header.length, 0)
184
- smallFrame.write(header, 4, "utf8")
185
- smallFrame.writeUInt32BE(5, 4 + header.length)
186
- smallFrame.write("hello", 4 + header.length + 4, "utf8")
184
+ const header = JSON.stringify({type: "small"})
185
+ const smallFrame = Buffer.alloc(4 + header.length + 4 + 5)
186
+ smallFrame.writeUInt32BE(header.length, 0)
187
+ smallFrame.write(header, 4, "utf8")
188
+ smallFrame.writeUInt32BE(5, 4 + header.length)
189
+ smallFrame.write("hello", 4 + header.length + 4, "utf8")
187
190
 
188
- const frames = decoder.push(smallFrame.subarray(0, 7))
189
- assert.deepEqual(frames, [])
191
+ const frames = decoder.push(smallFrame.subarray(0, 7))
192
+ expect(frames).toEqual([])
190
193
 
191
- const frames2 = decoder.push(smallFrame.subarray(7))
192
- assert.equal(frames2.length, 1)
193
- assert.equal(frames2[0]?.header.type, "small")
194
+ const frames2 = decoder.push(smallFrame.subarray(7))
195
+ expect(frames2.length).toBe(1)
196
+ expect(frames2[0]?.header.type).toBe("small")
194
197
 
195
- const big = Buffer.alloc(2000)
196
- big.writeUInt32BE(5, 0)
197
- assert.throws(() => decoder.push(big), /input byte budget/i)
198
- })
198
+ const big = Buffer.alloc(2000)
199
+ big.writeUInt32BE(5, 0)
200
+ expect(() => decoder.push(big)).toThrow(/input byte budget/i)
201
+ })
199
202
 
200
- test("FrameDecoder with zero maxInputBytes disables budget check", () => {
201
- const decoder = new FrameDecoder({maxInputBytes: 0, maxHeaderBytes: 200_000})
203
+ it("FrameDecoder with zero maxInputBytes disables budget check", () => {
204
+ const decoder = new FrameDecoder({maxInputBytes: 0, maxHeaderBytes: 200_000})
202
205
 
203
- const smallChunk = Buffer.alloc(100_001)
204
- smallChunk.writeUInt32BE(100_000, 0)
206
+ const smallChunk = Buffer.alloc(100_001)
207
+ smallChunk.writeUInt32BE(100_000, 0)
205
208
 
206
- assert.deepEqual(decoder.push(smallChunk), [])
207
- })
209
+ expect(decoder.push(smallChunk)).toEqual([])
210
+ })
208
211
 
209
- test("FrameDecoder byte-by-byte input exercises cursor path: one frame", () => {
210
- const payload = Buffer.from("cursor")
211
- const encoded = encodeFrame({type: "cursor-test"}, payload)
212
- const decoder = new FrameDecoder()
212
+ it("FrameDecoder byte-by-byte input exercises cursor path: one frame", () => {
213
+ const payload = Buffer.from("cursor")
214
+ const encoded = encodeFrame({type: "cursor-test"}, payload)
215
+ const decoder = new FrameDecoder()
213
216
 
214
- for (let i = 0; i < encoded.length; i++) {
215
- const result = decoder.push(encoded.subarray(i, i + 1))
216
- if (i < encoded.length - 1) {
217
- assert.deepEqual(result, [])
217
+ for (let i = 0; i < encoded.length; i++) {
218
+ const result = decoder.push(encoded.subarray(i, i + 1))
219
+ if (i < encoded.length - 1) {
220
+ expect(result).toEqual([])
221
+ }
218
222
  }
219
- }
220
223
 
221
- const frames = decoder.push(Buffer.alloc(0))
224
+ const frames = decoder.push(Buffer.alloc(0))
222
225
 
223
- assert.deepEqual(frames, [])
224
- })
226
+ expect(frames).toEqual([])
227
+ })
225
228
 
226
- test("FrameDecoder byte-by-byte input exercises cursor path: multiple queued frames", () => {
227
- const frame1 = encodeFrame({type: "alpha"}, Buffer.from("a"))
228
- const frame2 = encodeFrame({type: "beta"}, Buffer.from("bb"))
229
- const frame3 = encodeFrame({type: "gamma"}, Buffer.from("ccc"))
230
- const combined = Buffer.concat([frame1, frame2, frame3])
231
- const decoder = new FrameDecoder()
232
-
233
- /** @type {import('../../../src/protocol/frame.js').Frame[]} */
234
- const allFrames = []
235
- for (let i = 0; i < combined.length; i++) {
236
- const result = decoder.push(combined.subarray(i, i + 1))
237
- for (const f of result) {
229
+ it("FrameDecoder byte-by-byte input exercises cursor path: multiple queued frames", () => {
230
+ const frame1 = encodeFrame({type: "alpha"}, Buffer.from("a"))
231
+ const frame2 = encodeFrame({type: "beta"}, Buffer.from("bb"))
232
+ const frame3 = encodeFrame({type: "gamma"}, Buffer.from("ccc"))
233
+ const combined = Buffer.concat([frame1, frame2, frame3])
234
+ const decoder = new FrameDecoder()
235
+
236
+ /** @type {import('../../../src/protocol/frame.js').Frame[]} */
237
+ const allFrames = []
238
+ for (let i = 0; i < combined.length; i++) {
239
+ const result = decoder.push(combined.subarray(i, i + 1))
240
+ for (const f of result) {
241
+ allFrames.push(f)
242
+ }
243
+ }
244
+
245
+ for (const f of decoder.push(Buffer.alloc(0))) {
238
246
  allFrames.push(f)
239
247
  }
240
- }
241
-
242
- for (const f of decoder.push(Buffer.alloc(0))) {
243
- allFrames.push(f)
244
- }
245
-
246
- assert.equal(allFrames.length, 3)
247
- assert.equal(allFrames[0]?.header.type, "alpha")
248
- assert.equal(allFrames[0]?.payload.toString(), "a")
249
- assert.equal(allFrames[1]?.header.type, "beta")
250
- assert.equal(allFrames[1]?.payload.toString(), "bb")
251
- assert.equal(allFrames[2]?.header.type, "gamma")
252
- assert.equal(allFrames[2]?.payload.toString(), "ccc")
253
- })
254
248
 
255
- test("FrameDecoder discards header read from garbage bytes and throws on oversized header", () => {
256
- const decoder = new FrameDecoder()
249
+ expect(allFrames.length).toBe(3)
250
+ expect(allFrames[0]?.header.type).toBe("alpha")
251
+ expect(allFrames[0]?.payload.toString()).toBe("a")
252
+ expect(allFrames[1]?.header.type).toBe("beta")
253
+ expect(allFrames[1]?.payload.toString()).toBe("bb")
254
+ expect(allFrames[2]?.header.type).toBe("gamma")
255
+ expect(allFrames[2]?.payload.toString()).toBe("ccc")
256
+ })
257
257
 
258
- const garbage = Buffer.alloc(5000, 0xFF)
259
- assert.throws(() => decoder.push(garbage), /header length/i)
260
- })
258
+ it("FrameDecoder discards header read from garbage bytes and throws on oversized header", () => {
259
+ const decoder = new FrameDecoder()
261
260
 
262
- test("FrameDecoder compaction path: readPos shifts after partial consumption triggers compact", () => {
263
- const frame1 = encodeFrame({type: "first"}, Buffer.from("data1"))
264
- const frame2 = encodeFrame({type: "second"}, Buffer.from("data2"))
265
- const combined = Buffer.concat([frame1, frame2])
266
- const decoder = new FrameDecoder()
267
-
268
- /** @type {import('../../../src/protocol/frame.js').Frame[]} */
269
- const allFrames = []
270
- for (const f of decoder.push(combined.subarray(0, frame1.length + 2))) {
271
- allFrames.push(f)
272
- }
273
-
274
- for (const f of decoder.push(combined.subarray(frame1.length + 2))) {
275
- allFrames.push(f)
276
- }
277
-
278
- for (const f of decoder.push(Buffer.alloc(0))) {
279
- allFrames.push(f)
280
- }
281
-
282
- assert.equal(allFrames.length, 2)
283
- assert.equal(allFrames[0]?.header.type, "first")
284
- assert.equal(allFrames[1]?.header.type, "second")
285
- })
261
+ const garbage = Buffer.alloc(5000, 0xFF)
262
+ expect(() => decoder.push(garbage)).toThrow(/header length/i)
263
+ })
286
264
 
287
- test("FrameDecoder verifies the decoder is reusable across many byte-by-byte deliveries", () => {
288
- const payload = Buffer.from("large-frame")
289
- const encoded = encodeFrame({type: "reuse"}, payload)
290
- const decoder = new FrameDecoder()
265
+ it("FrameDecoder compaction path: readPos shifts after partial consumption triggers compact", () => {
266
+ const frame1 = encodeFrame({type: "first"}, Buffer.from("data1"))
267
+ const frame2 = encodeFrame({type: "second"}, Buffer.from("data2"))
268
+ const combined = Buffer.concat([frame1, frame2])
269
+ const decoder = new FrameDecoder()
291
270
 
292
- let totalFrames = 0
293
- for (let round = 0; round < 5; round++) {
294
- for (let i = 0; i < encoded.length; i++) {
295
- const result = decoder.push(encoded.subarray(i, i + 1))
296
- totalFrames += result.length
271
+ /** @type {import('../../../src/protocol/frame.js').Frame[]} */
272
+ const allFrames = []
273
+ for (const f of decoder.push(combined.subarray(0, frame1.length + 2))) {
274
+ allFrames.push(f)
297
275
  }
298
- }
299
276
 
300
- totalFrames += decoder.push(Buffer.alloc(0)).length
277
+ for (const f of decoder.push(combined.subarray(frame1.length + 2))) {
278
+ allFrames.push(f)
279
+ }
301
280
 
302
- assert.equal(totalFrames, 5)
303
- })
281
+ for (const f of decoder.push(Buffer.alloc(0))) {
282
+ allFrames.push(f)
283
+ }
304
284
 
305
- test("FrameDecoder disableInputBudget allows exceeding original budget", () => {
306
- const decoder = new FrameDecoder({maxInputBytes: 100})
307
- const smallFrame = encodeFrame({type: "small"}, Buffer.from("hello"))
308
- decoder.push(smallFrame)
285
+ expect(allFrames.length).toBe(2)
286
+ expect(allFrames[0]?.header.type).toBe("first")
287
+ expect(allFrames[1]?.header.type).toBe("second")
288
+ })
309
289
 
310
- decoder.disableInputBudget()
290
+ it("FrameDecoder verifies the decoder is reusable across many byte-by-byte deliveries", () => {
291
+ const payload = Buffer.from("large-frame")
292
+ const encoded = encodeFrame({type: "reuse"}, payload)
293
+ const decoder = new FrameDecoder()
294
+
295
+ let totalFrames = 0
296
+ for (let round = 0; round < 5; round++) {
297
+ for (let i = 0; i < encoded.length; i++) {
298
+ const result = decoder.push(encoded.subarray(i, i + 1))
299
+ totalFrames += result.length
300
+ }
301
+ }
311
302
 
312
- const largeFrame = encodeFrame({type: "large"}, Buffer.allocUnsafe(200_000))
313
- const after = decoder.push(largeFrame)
314
- assert.equal(after.length, 1)
315
- assert.equal(after[0]?.header.type, "large")
316
- })
303
+ totalFrames += decoder.push(Buffer.alloc(0)).length
317
304
 
318
- test("FrameDecoder compaction reuses existing capacity instead of unbounded growth", () => {
319
- const decoder = new FrameDecoder()
305
+ expect(totalFrames).toBe(5)
306
+ })
307
+
308
+ it("FrameDecoder disableInputBudget allows exceeding original budget", () => {
309
+ const decoder = new FrameDecoder({maxInputBytes: 100})
310
+ const smallFrame = encodeFrame({type: "small"}, Buffer.from("hello"))
311
+ decoder.push(smallFrame)
312
+
313
+ decoder.disableInputBudget()
314
+
315
+ const largeFrame = encodeFrame({type: "large"}, Buffer.allocUnsafe(200_000))
316
+ const after = decoder.push(largeFrame)
317
+ expect(after.length).toBe(1)
318
+ expect(after[0]?.header.type).toBe("large")
319
+ })
320
+
321
+ it("FrameDecoder compaction reuses existing capacity instead of unbounded growth", () => {
322
+ const decoder = new FrameDecoder()
320
323
 
321
- const manyBytes = Buffer.alloc(1000, 0x41)
322
- const bigFrame = encodeFrame({type: "big"}, manyBytes)
324
+ const manyBytes = Buffer.alloc(1000, 0x41)
325
+ const bigFrame = encodeFrame({type: "big"}, manyBytes)
323
326
 
324
- const part1 = bigFrame.subarray(0, 800)
325
- const part2 = bigFrame.subarray(800)
326
- decoder.push(part1)
327
- const extracted = decoder.push(part2)
328
- assert.equal(extracted.length, 1)
327
+ const part1 = bigFrame.subarray(0, 800)
328
+ const part2 = bigFrame.subarray(800)
329
+ decoder.push(part1)
330
+ const extracted = decoder.push(part2)
331
+ expect(extracted.length).toBe(1)
329
332
 
330
- for (let round = 0; round < 20; round++) {
331
- const inner = encodeFrame({type: "small"}, Buffer.from("x"))
332
- const result = decoder.push(inner)
333
- assert.equal(result.length, 1)
334
- assert.equal(result[0]?.header.type, "small")
335
- }
333
+ for (let round = 0; round < 20; round++) {
334
+ const inner = encodeFrame({type: "small"}, Buffer.from("x"))
335
+ const result = decoder.push(inner)
336
+ expect(result.length).toBe(1)
337
+ expect(result[0]?.header.type).toBe("small")
338
+ }
339
+ })
336
340
  })