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,5 +1,4 @@
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 {InMemoryUserStore, login, TokenStore} from "../../../src/auth/user-store.js"
5
4
  import {handleControlMessage as rawHandleControlMessage} from "../../../src/server/control-handler.js"
@@ -26,266 +25,268 @@ function handleControlMessage(options, message) {
26
25
  return rawHandleControlMessage({...options, supportedFraming: [2]}, {...message, framing: [2]})
27
26
  }
28
27
 
29
- test("handleControlMessage requires auth before creating sessions", () => {
30
- const response = handleControlMessage({tokens: new TokenStore(), sessions: new SessionStore()}, {
31
- type: "session.create",
32
- name: "laptop shell"
28
+ describe("control handler", () => {
29
+ it("handleControlMessage requires auth before creating sessions", () => {
30
+ const response = handleControlMessage({tokens: new TokenStore(), sessions: new SessionStore()}, {
31
+ type: "session.create",
32
+ name: "laptop shell"
33
+ })
34
+
35
+ expect(response).toEqual({
36
+ type: "error",
37
+ code: "AUTH_REQUIRED",
38
+ message: "Authentication is required.",
39
+ retryable: false
40
+ })
33
41
  })
34
42
 
35
- assert.deepEqual(response, {
36
- type: "error",
37
- code: "AUTH_REQUIRED",
38
- message: "Authentication is required.",
39
- retryable: false
43
+ it("handleControlMessage creates a named session for authenticated users", async () => {
44
+ const {token, tokens, sessions} = await createAuthenticatedStores()
45
+
46
+ const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
47
+ type: "session.create",
48
+ token: token.value,
49
+ name: "laptop shell",
50
+ identifier: "session-123",
51
+ target: {host: "127.0.0.1", port: 5432}
52
+ })
53
+
54
+ expect(response).toEqual({
55
+ type: "session.created",
56
+ identifier: "session-123",
57
+ name: "laptop shell",
58
+ state: "creating",
59
+ target: {host: "127.0.0.1", port: 5432},
60
+ framing: 2
61
+ })
40
62
  })
41
- })
42
-
43
- test("handleControlMessage creates a named session for authenticated users", async () => {
44
- const {token, tokens, sessions} = await createAuthenticatedStores()
45
63
 
46
- const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
47
- type: "session.create",
48
- token: token.value,
49
- name: "laptop shell",
50
- identifier: "session-123",
51
- target: {host: "127.0.0.1", port: 5432}
64
+ it("handleControlMessage returns a protocol error for duplicate session identifiers", async () => {
65
+ const {token, tokens, sessions} = await createAuthenticatedStores()
66
+ const message = {
67
+ type: "session.create",
68
+ token: token.value,
69
+ name: "laptop shell",
70
+ identifier: "session-123",
71
+ target: {host: "127.0.0.1", port: 5432}
72
+ }
73
+
74
+ expect(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, message).type).toBe("session.created")
75
+ expect(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, message)).toEqual({
76
+ type: "error",
77
+ code: "SESSION_EXISTS",
78
+ message: "Session identifier already exists.",
79
+ retryable: false
80
+ })
52
81
  })
53
82
 
54
- assert.deepEqual(response, {
55
- type: "session.created",
56
- identifier: "session-123",
57
- name: "laptop shell",
58
- state: "creating",
59
- target: {host: "127.0.0.1", port: 5432},
60
- framing: 2
83
+ it("handleControlMessage resumes only sessions owned by the authenticated user", async () => {
84
+ const {token, tokens, sessions} = await createAuthenticatedStores()
85
+ const created = handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
86
+ type: "session.create",
87
+ token: token.value,
88
+ name: "laptop shell",
89
+ identifier: "session-123",
90
+ target: {host: "127.0.0.1", port: 5432}
91
+ })
92
+ expect(created.type).toBe("session.created")
93
+ sessions.transition("session-123", "active")
94
+ sessions.updateSequences("session-123", {clientSequence: 42, relaySequence: 7})
95
+ sessions.transition("session-123", "interrupted")
96
+
97
+ const resumed = handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
98
+ type: "session.resume",
99
+ token: token.value,
100
+ identifier: "session-123"
101
+ })
102
+
103
+ expect(resumed).toEqual({
104
+ type: "session.resumed",
105
+ identifier: "session-123",
106
+ name: "laptop shell",
107
+ state: "resuming",
108
+ serverLastSequence: 7,
109
+ serverLastAck: 42,
110
+ replayFromSequence: 43,
111
+ target: {host: "127.0.0.1", port: 5432},
112
+ framing: 2
113
+ })
61
114
  })
62
- })
63
115
 
64
- test("handleControlMessage returns a protocol error for duplicate session identifiers", async () => {
65
- const {token, tokens, sessions} = await createAuthenticatedStores()
66
- const message = {
67
- type: "session.create",
68
- token: token.value,
69
- name: "laptop shell",
70
- identifier: "session-123",
71
- target: {host: "127.0.0.1", port: 5432}
72
- }
73
-
74
- assert.equal(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, message).type, "session.created")
75
- assert.deepEqual(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, message), {
76
- type: "error",
77
- code: "SESSION_EXISTS",
78
- message: "Session identifier already exists.",
79
- retryable: false
116
+ it("handleControlMessage rejects missing or malformed session targets", async () => {
117
+ const {token, tokens, sessions} = await createAuthenticatedStores()
118
+
119
+ expect(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
120
+ type: "session.create",
121
+ token: token.value,
122
+ name: "missing target"
123
+ })).toEqual({
124
+ type: "error",
125
+ code: "PROTOCOL_VIOLATION",
126
+ message: "Target host and port are required.",
127
+ retryable: false
128
+ })
129
+
130
+ expect(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
131
+ type: "session.create",
132
+ token: token.value,
133
+ name: "bad target",
134
+ target: {host: "", port: 0}
135
+ })).toEqual({
136
+ type: "error",
137
+ code: "PROTOCOL_VIOLATION",
138
+ message: "Target host and port are required.",
139
+ retryable: false
140
+ })
80
141
  })
81
- })
82
142
 
83
- test("handleControlMessage resumes only sessions owned by the authenticated user", async () => {
84
- const {token, tokens, sessions} = await createAuthenticatedStores()
85
- const created = handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
86
- type: "session.create",
87
- token: token.value,
88
- name: "laptop shell",
89
- identifier: "session-123",
90
- target: {host: "127.0.0.1", port: 5432}
91
- })
92
- assert.equal(created.type, "session.created")
93
- sessions.transition("session-123", "active")
94
- sessions.updateSequences("session-123", {clientSequence: 42, relaySequence: 7})
95
- sessions.transition("session-123", "interrupted")
96
-
97
- const resumed = handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
98
- type: "session.resume",
99
- token: token.value,
100
- identifier: "session-123"
143
+ it("handleControlMessage rejects unauthorized targets", async () => {
144
+ const {token, tokens, sessions} = await createAuthenticatedStores()
145
+
146
+ const response = handleControlMessage({
147
+ tokens,
148
+ sessions,
149
+ authorizeTarget: () => false
150
+ }, {
151
+ type: "session.create",
152
+ token: token.value,
153
+ name: "blocked target",
154
+ target: {host: "10.0.0.1", port: 22}
155
+ })
156
+
157
+ expect(response).toEqual({
158
+ type: "error",
159
+ code: "TARGET_UNAUTHORIZED",
160
+ message: "Target is not authorized for this principal.",
161
+ retryable: false
162
+ })
101
163
  })
102
164
 
103
- assert.deepEqual(resumed, {
104
- type: "session.resumed",
105
- identifier: "session-123",
106
- name: "laptop shell",
107
- state: "resuming",
108
- serverLastSequence: 7,
109
- serverLastAck: 42,
110
- replayFromSequence: 43,
111
- target: {host: "127.0.0.1", port: 5432},
112
- framing: 2
113
- })
114
- })
165
+ it("handleControlMessage selects a mutually supported stream.data codec on create", async () => {
166
+ const {token, tokens, sessions} = await createAuthenticatedStores()
115
167
 
116
- test("handleControlMessage rejects missing or malformed session targets", async () => {
117
- const {token, tokens, sessions} = await createAuthenticatedStores()
118
-
119
- assert.deepEqual(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
120
- type: "session.create",
121
- token: token.value,
122
- name: "missing target"
123
- }), {
124
- type: "error",
125
- code: "PROTOCOL_VIOLATION",
126
- message: "Target host and port are required.",
127
- retryable: false
128
- })
129
-
130
- assert.deepEqual(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
131
- type: "session.create",
132
- token: token.value,
133
- name: "bad target",
134
- target: {host: "", port: 0}
135
- }), {
136
- type: "error",
137
- code: "PROTOCOL_VIOLATION",
138
- message: "Target host and port are required.",
139
- retryable: false
140
- })
141
- })
168
+ const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
169
+ type: "session.create",
170
+ token: token.value,
171
+ name: "compressed",
172
+ target: {host: "127.0.0.1", port: 5432},
173
+ compression: ["zstd"]
174
+ })
142
175
 
143
- test("handleControlMessage rejects unauthorized targets", async () => {
144
- const {token, tokens, sessions} = await createAuthenticatedStores()
145
-
146
- const response = handleControlMessage({
147
- tokens,
148
- sessions,
149
- authorizeTarget: () => false
150
- }, {
151
- type: "session.create",
152
- token: token.value,
153
- name: "blocked target",
154
- target: {host: "10.0.0.1", port: 22}
176
+ expect(response.type).toBe("session.created")
177
+ expect(response.compression).toBe("zstd")
155
178
  })
156
179
 
157
- assert.deepEqual(response, {
158
- type: "error",
159
- code: "TARGET_UNAUTHORIZED",
160
- message: "Target is not authorized for this principal.",
161
- retryable: false
162
- })
163
- })
180
+ it("handleControlMessage omits compression for a legacy client that offers none", async () => {
181
+ const {token, tokens, sessions} = await createAuthenticatedStores()
164
182
 
165
- test("handleControlMessage selects a mutually supported stream.data codec on create", async () => {
166
- const {token, tokens, sessions} = await createAuthenticatedStores()
183
+ const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
184
+ type: "session.create",
185
+ token: token.value,
186
+ name: "legacy",
187
+ target: {host: "127.0.0.1", port: 5432}
188
+ })
167
189
 
168
- const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
169
- type: "session.create",
170
- token: token.value,
171
- name: "compressed",
172
- target: {host: "127.0.0.1", port: 5432},
173
- compression: ["zstd"]
190
+ expect(response.type).toBe("session.created")
191
+ expect("compression" in response).toBe(false)
174
192
  })
175
193
 
176
- assert.equal(response.type, "session.created")
177
- assert.equal(response.compression, "zstd")
178
- })
194
+ it("handleControlMessage omits compression when the relay supports no codec", async () => {
195
+ const {token, tokens, sessions} = await createAuthenticatedStores()
179
196
 
180
- test("handleControlMessage omits compression for a legacy client that offers none", async () => {
181
- const {token, tokens, sessions} = await createAuthenticatedStores()
197
+ const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: []}, {
198
+ type: "session.create",
199
+ token: token.value,
200
+ name: "disabled",
201
+ target: {host: "127.0.0.1", port: 5432},
202
+ compression: ["zstd"]
203
+ })
182
204
 
183
- const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
184
- type: "session.create",
185
- token: token.value,
186
- name: "legacy",
187
- target: {host: "127.0.0.1", port: 5432}
205
+ expect(response.type).toBe("session.created")
206
+ expect("compression" in response).toBe(false)
188
207
  })
189
208
 
190
- assert.equal(response.type, "session.created")
191
- assert.equal("compression" in response, false)
192
- })
209
+ it("handleControlMessage omits compression when no offered codec is supported", async () => {
210
+ const {token, tokens, sessions} = await createAuthenticatedStores()
193
211
 
194
- test("handleControlMessage omits compression when the relay supports no codec", async () => {
195
- const {token, tokens, sessions} = await createAuthenticatedStores()
212
+ const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
213
+ type: "session.create",
214
+ token: token.value,
215
+ name: "unknown codec",
216
+ target: {host: "127.0.0.1", port: 5432},
217
+ compression: ["brotli"]
218
+ })
196
219
 
197
- const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: []}, {
198
- type: "session.create",
199
- token: token.value,
200
- name: "disabled",
201
- target: {host: "127.0.0.1", port: 5432},
202
- compression: ["zstd"]
220
+ expect(response.type).toBe("session.created")
221
+ expect("compression" in response).toBe(false)
203
222
  })
204
223
 
205
- assert.equal(response.type, "session.created")
206
- assert.equal("compression" in response, false)
207
- })
208
-
209
- test("handleControlMessage omits compression when no offered codec is supported", async () => {
210
- const {token, tokens, sessions} = await createAuthenticatedStores()
211
-
212
- const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
213
- type: "session.create",
214
- token: token.value,
215
- name: "unknown codec",
216
- target: {host: "127.0.0.1", port: 5432},
217
- compression: ["brotli"]
224
+ it("handleControlMessage renegotiates the stream.data codec on resume", async () => {
225
+ const {token, tokens, sessions} = await createAuthenticatedStores()
226
+
227
+ const created = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
228
+ type: "session.create",
229
+ token: token.value,
230
+ name: "resumable",
231
+ target: {host: "127.0.0.1", port: 5432},
232
+ compression: ["zstd"]
233
+ })
234
+ expect(created.type).toBe("session.created")
235
+ const identifier = /** @type {string} */ (created.identifier)
236
+ sessions.transition(identifier, "active")
237
+
238
+ const resumed = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
239
+ type: "session.resume",
240
+ token: token.value,
241
+ identifier,
242
+ compression: ["zstd"]
243
+ })
244
+
245
+ expect(resumed.type).toBe("session.resumed")
246
+ expect(resumed.compression).toBe("zstd")
218
247
  })
219
248
 
220
- assert.equal(response.type, "session.created")
221
- assert.equal("compression" in response, false)
222
- })
223
-
224
- test("handleControlMessage renegotiates the stream.data codec on resume", async () => {
225
- const {token, tokens, sessions} = await createAuthenticatedStores()
226
-
227
- const created = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
228
- type: "session.create",
229
- token: token.value,
230
- name: "resumable",
231
- target: {host: "127.0.0.1", port: 5432},
232
- compression: ["zstd"]
233
- })
234
- assert.equal(created.type, "session.created")
235
- const identifier = /** @type {string} */ (created.identifier)
236
- sessions.transition(identifier, "active")
237
-
238
- const resumed = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
239
- type: "session.resume",
240
- token: token.value,
241
- identifier,
242
- compression: ["zstd"]
249
+ it("handleControlMessage negotiates compact framing v2 only when offered and supported", async () => {
250
+ const first = await createAuthenticatedStores()
251
+ const base = {type: "session.create", token: first.token.value, name: "compact", target: {host: "127.0.0.1", port: 80}}
252
+ const selected = rawHandleControlMessage({tokens: first.tokens, sessions: first.sessions, authorizeTarget: () => true, supportedFraming: [2]}, {...base, framing: [2]})
253
+ expect(selected.framing).toBe(2)
254
+
255
+ const second = await createAuthenticatedStores()
256
+ const missing = rawHandleControlMessage({tokens: second.tokens, sessions: second.sessions, authorizeTarget: () => true, supportedFraming: [2]}, {...base, token: second.token.value, identifier: "missing-framing"})
257
+ expect(missing.type).toBe("error")
258
+ expect(missing.code).toBe("PROTOCOL_VIOLATION")
259
+ expect(second.sessions.get("missing-framing")).toBeUndefined()
260
+
261
+ const third = await createAuthenticatedStores()
262
+ const legacy = rawHandleControlMessage({tokens: third.tokens, sessions: third.sessions, authorizeTarget: () => true, supportedFraming: [2]}, {...base, token: third.token.value, identifier: "legacy-framing", framing: [1]})
263
+ expect(legacy.type).toBe("error")
264
+ expect(legacy.code).toBe("PROTOCOL_VIOLATION")
265
+ expect(third.sessions.get("legacy-framing")).toBeUndefined()
243
266
  })
244
267
 
245
- assert.equal(resumed.type, "session.resumed")
246
- assert.equal(resumed.compression, "zstd")
247
- })
248
-
249
- test("handleControlMessage negotiates compact framing v2 only when offered and supported", async () => {
250
- const first = await createAuthenticatedStores()
251
- const base = {type: "session.create", token: first.token.value, name: "compact", target: {host: "127.0.0.1", port: 80}}
252
- const selected = rawHandleControlMessage({tokens: first.tokens, sessions: first.sessions, authorizeTarget: () => true, supportedFraming: [2]}, {...base, framing: [2]})
253
- assert.equal(selected.framing, 2)
254
-
255
- const second = await createAuthenticatedStores()
256
- const missing = rawHandleControlMessage({tokens: second.tokens, sessions: second.sessions, authorizeTarget: () => true, supportedFraming: [2]}, {...base, token: second.token.value, identifier: "missing-framing"})
257
- assert.equal(missing.type, "error")
258
- assert.equal(missing.code, "PROTOCOL_VIOLATION")
259
- assert.equal(second.sessions.get("missing-framing"), undefined)
260
-
261
- const third = await createAuthenticatedStores()
262
- const legacy = rawHandleControlMessage({tokens: third.tokens, sessions: third.sessions, authorizeTarget: () => true, supportedFraming: [2]}, {...base, token: third.token.value, identifier: "legacy-framing", framing: [1]})
263
- assert.equal(legacy.type, "error")
264
- assert.equal(legacy.code, "PROTOCOL_VIOLATION")
265
- assert.equal(third.sessions.get("legacy-framing"), undefined)
266
- })
267
-
268
- test("handleControlMessage rejects legacy resume before authentication or session mutation", async () => {
269
- const {token, tokens, sessions} = await createAuthenticatedStores()
270
- handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
271
- type: "session.create",
272
- token: token.value,
273
- name: "resumable",
274
- identifier: "resume-framing",
275
- target: {host: "127.0.0.1", port: 80}
268
+ it("handleControlMessage rejects legacy resume before authentication or session mutation", async () => {
269
+ const {token, tokens, sessions} = await createAuthenticatedStores()
270
+ handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
271
+ type: "session.create",
272
+ token: token.value,
273
+ name: "resumable",
274
+ identifier: "resume-framing",
275
+ target: {host: "127.0.0.1", port: 80}
276
+ })
277
+ sessions.transition("resume-framing", "active")
278
+ sessions.transition("resume-framing", "interrupted")
279
+
280
+ let authenticationCalls = 0
281
+ const response = rawHandleControlMessage({
282
+ tokens: {authenticate: () => { authenticationCalls += 1; return {userId: "ignored", username: "ignored"} }},
283
+ sessions,
284
+ supportedFraming: [2]
285
+ }, {type: "session.resume", token: "ignored", identifier: "resume-framing", framing: [1]})
286
+
287
+ expect(response.type).toBe("error")
288
+ expect(response.code).toBe("PROTOCOL_VIOLATION")
289
+ expect(authenticationCalls).toBe(0)
290
+ expect(sessions.get("resume-framing")?.state).toBe("interrupted")
276
291
  })
277
- sessions.transition("resume-framing", "active")
278
- sessions.transition("resume-framing", "interrupted")
279
-
280
- let authenticationCalls = 0
281
- const response = rawHandleControlMessage({
282
- tokens: {authenticate: () => { authenticationCalls += 1; return {userId: "ignored", username: "ignored"} }},
283
- sessions,
284
- supportedFraming: [2]
285
- }, {type: "session.resume", token: "ignored", identifier: "resume-framing", framing: [1]})
286
-
287
- assert.equal(response.type, "error")
288
- assert.equal(response.code, "PROTOCOL_VIOLATION")
289
- assert.equal(authenticationCalls, 0)
290
- assert.equal(sessions.get("resume-framing")?.state, "interrupted")
291
292
  })
@@ -1,59 +1,61 @@
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 {resolveWorkerCount, startPrimary} from "../../../src/server/primary.js"
5
4
 
6
- test("resolveWorkerCount uses configured workers or available CPUs", () => {
7
- assert.equal(resolveWorkerCount(3, () => 99), 3)
8
- assert.equal(resolveWorkerCount(undefined, () => 4), 4)
9
- assert.equal(resolveWorkerCount(undefined, () => 0), 1)
10
- assert.throws(() => resolveWorkerCount(0), /Invalid worker count/)
11
- })
5
+ describe("primary", () => {
6
+ it("resolveWorkerCount uses configured workers or available CPUs", () => {
7
+ expect(resolveWorkerCount(3, () => 99)).toBe(3)
8
+ expect(resolveWorkerCount(undefined, () => 4)).toBe(4)
9
+ expect(resolveWorkerCount(undefined, () => 0)).toBe(1)
10
+ expect(() => resolveWorkerCount(0)).toThrow(/Invalid worker count/)
11
+ })
12
12
 
13
- test("startPrimary forks configured workers and restarts exits", () => {
14
- /** @type {Array<Record<string, string> | undefined>} */
15
- const forks = []
16
- /** @type {undefined | ((worker: {id: number}, code: number | null, signal: string | null) => void)} */
17
- let exitListener
18
- /** @type {string[]} */
19
- const warnings = []
20
-
21
- const result = startPrimary({
22
- workers: 2,
23
- clusterApi: {
24
- isPrimary: true,
25
- fork: (env) => {
26
- forks.push(env)
27
- return {id: forks.length}
13
+ it("startPrimary forks configured workers and restarts exits", () => {
14
+ /** @type {Array<Record<string, string> | undefined>} */
15
+ const forks = []
16
+ /** @type {undefined | ((worker: {id: number}, code: number | null, signal: string | null) => void)} */
17
+ let exitListener
18
+ /** @type {string[]} */
19
+ const warnings = []
20
+
21
+ const result = startPrimary({
22
+ workers: 2,
23
+ clusterApi: {
24
+ isPrimary: true,
25
+ fork: (env) => {
26
+ forks.push(env)
27
+ return {id: forks.length}
28
+ },
29
+ on: (_event, listener) => {
30
+ exitListener = listener
31
+ }
28
32
  },
29
- on: (_event, listener) => {
30
- exitListener = listener
31
- }
32
- },
33
- logger: {warn: (message) => warnings.push(message)}
33
+ logger: {warn: (message) => warnings.push(message)}
34
+ })
35
+
36
+ expect(result).toEqual({role: "primary", workers: 2})
37
+ expect(forks).toEqual([{SOCKETDUCT_WORKER_INDEX: "0"}, {SOCKETDUCT_WORKER_INDEX: "1"}])
38
+
39
+ expect(exitListener).toBeDefined()
40
+ if (exitListener === undefined) throw new Error("Expected an exit listener")
41
+ exitListener({id: 7}, 1, null)
42
+ expect(forks.length).toBe(3)
43
+ expect(warnings.length).toBe(1)
44
+ expect(String(warnings[0])).toMatch(/worker 7 exited/)
34
45
  })
35
46
 
36
- assert.deepEqual(result, {role: "primary", workers: 2})
37
- assert.deepEqual(forks, [{SOCKETDUCT_WORKER_INDEX: "0"}, {SOCKETDUCT_WORKER_INDEX: "1"}])
38
-
39
- assert.ok(exitListener)
40
- exitListener({id: 7}, 1, null)
41
- assert.equal(forks.length, 3)
42
- assert.equal(warnings.length, 1)
43
- assert.match(String(warnings[0]), /worker 7 exited/)
44
- })
47
+ it("startPrimary reports worker role without forking", () => {
48
+ const result = startPrimary({
49
+ workers: 2,
50
+ clusterApi: {
51
+ isPrimary: false,
52
+ fork: () => {
53
+ throw new Error("should not fork from worker")
54
+ },
55
+ on: () => {}
56
+ }
57
+ })
45
58
 
46
- test("startPrimary reports worker role without forking", () => {
47
- const result = startPrimary({
48
- workers: 2,
49
- clusterApi: {
50
- isPrimary: false,
51
- fork: () => {
52
- throw new Error("should not fork from worker")
53
- },
54
- on: () => {}
55
- }
59
+ expect(result).toEqual({role: "worker", workers: 0})
56
60
  })
57
-
58
- assert.deepEqual(result, {role: "worker", workers: 0})
59
61
  })