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,31 +1,29 @@
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 {ZSTD_CODEC, zstdAvailable} from "../../../src/protocol/compression.js"
5
4
  import {AdaptiveCompressor, decodeV3Data, encodeV3Data} from "../../../src/protocol/v3-compression.js"
6
5
  import {V3FrameFlag} from "../../../src/protocol/v3-frame.js"
7
6
 
8
- test("v3 reverse DATA compression is independent, bounded, and negotiated", async (t) => {
9
- if (!zstdAvailable()) {
10
- t.skip("Node runtime has no zstd binding")
11
- return
12
- }
13
- const raw = Buffer.from(Array.from({length: 4096}, (_, index) => `{"dockerEvent":${index},"status":"running"}\n`).join(""))
14
- const encoded = await encodeV3Data(raw, ZSTD_CODEC, new AdaptiveCompressor())
15
- assert.equal(encoded.flags, V3FrameFlag.DATA_COMPRESSED)
16
- assert(encoded.wireBytes < encoded.rawBytes)
17
- assert.deepEqual(await decodeV3Data(encoded, ZSTD_CODEC), raw)
18
- await assert.rejects(decodeV3Data(encoded, undefined), /not negotiated/i)
7
+ describe("v3 DATA compression", () => {
8
+ it("v3 reverse DATA compression is independent, bounded, and negotiated", {skip: !zstdAvailable()}, async () => {
9
+ // Skipped when the Node runtime has no zstd binding.
10
+ const raw = Buffer.from(Array.from({length: 4096}, (_, index) => `{"dockerEvent":${index},"status":"running"}\n`).join(""))
11
+ const encoded = await encodeV3Data(raw, ZSTD_CODEC, new AdaptiveCompressor())
12
+ expect(encoded.flags).toBe(V3FrameFlag.DATA_COMPRESSED)
13
+ expect(encoded.wireBytes < encoded.rawBytes).toBeTruthy()
14
+ expect(Buffer.from(await decodeV3Data(encoded, ZSTD_CODEC)).equals(Buffer.from(raw))).toBe(true)
15
+ await expect(decodeV3Data(encoded, undefined)).rejects.toThrow(/not negotiated/i)
19
16
 
20
- const corrupt = {...encoded, payload: Buffer.from(encoded.payload)}
21
- const last = corrupt.payload.length - 1
22
- corrupt.payload[last] = /** @type {number} */ (corrupt.payload[last]) ^ 0xff
23
- await assert.rejects(decodeV3Data(corrupt, ZSTD_CODEC), /decompress/i)
24
- })
17
+ const corrupt = {...encoded, payload: Buffer.from(encoded.payload)}
18
+ const last = corrupt.payload.length - 1
19
+ corrupt.payload[last] = /** @type {number} */ (corrupt.payload[last]) ^ 0xff
20
+ await expect(decodeV3Data(corrupt, ZSTD_CODEC)).rejects.toThrow(/decompress/i)
21
+ })
25
22
 
26
- test("v3 reverse DATA remains raw when adaptive compression cannot save bytes", async () => {
27
- const raw = Buffer.from("small")
28
- const encoded = await encodeV3Data(raw, ZSTD_CODEC, new AdaptiveCompressor())
29
- assert.equal(encoded.flags, 0)
30
- assert.deepEqual(await decodeV3Data(encoded, ZSTD_CODEC), raw)
23
+ it("v3 reverse DATA remains raw when adaptive compression cannot save bytes", async () => {
24
+ const raw = Buffer.from("small")
25
+ const encoded = await encodeV3Data(raw, ZSTD_CODEC, new AdaptiveCompressor())
26
+ expect(encoded.flags).toBe(0)
27
+ expect(Buffer.from(await decodeV3Data(encoded, ZSTD_CODEC)).equals(Buffer.from(raw))).toBe(true)
28
+ })
31
29
  })
@@ -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 {
5
4
  decodeV3Control,
@@ -10,48 +9,50 @@ import {
10
9
  V3FrameType
11
10
  } from "../../../src/protocol/v3-frame.js"
12
11
 
13
- test("v3 decoder parses fragmented and coalesced binary frames", () => {
14
- const first = encodeV3Frame({type: V3FrameType.PING, streamId: 0, sequence: 7})
15
- const second = encodeV3Frame({type: V3FrameType.DATA, streamId: 1, sequence: 3}, Buffer.from("abc"))
16
- const wire = Buffer.concat([first, second])
17
- const decoder = new V3FrameDecoder({maxFrameBytes: 64})
12
+ describe("v3 frame protocol", () => {
13
+ it("v3 decoder parses fragmented and coalesced binary frames", () => {
14
+ const first = encodeV3Frame({type: V3FrameType.PING, streamId: 0, sequence: 7})
15
+ const second = encodeV3Frame({type: V3FrameType.DATA, streamId: 1, sequence: 3}, Buffer.from("abc"))
16
+ const wire = Buffer.concat([first, second])
17
+ const decoder = new V3FrameDecoder({maxFrameBytes: 64})
18
18
 
19
- assert.deepEqual(decoder.push(wire.subarray(0, 9)), [])
20
- const frames = decoder.push(wire.subarray(9))
21
- assert.deepEqual(frames, [
22
- {type: V3FrameType.PING, flags: 0, streamId: 0, sequence: 7, payload: Buffer.alloc(0)},
23
- {type: V3FrameType.DATA, flags: 0, streamId: 1, sequence: 3, payload: Buffer.from("abc")}
24
- ])
25
- })
19
+ expect(decoder.push(wire.subarray(0, 9))).toEqual([])
20
+ const frames = decoder.push(wire.subarray(9))
21
+ const [pingFrame, dataFrame] = frames
22
+ expect(pingFrame?.type).toBe(V3FrameType.PING)
23
+ expect(pingFrame?.flags).toBe(0)
24
+ expect(pingFrame?.streamId).toBe(0)
25
+ expect(pingFrame?.sequence).toBe(7)
26
+ if (!pingFrame) throw new Error("expected a decoded v3 PING frame")
27
+ expect(Buffer.from(pingFrame.payload).toString("hex")).toBe(Buffer.alloc(0).toString("hex"))
28
+ expect(dataFrame?.type).toBe(V3FrameType.DATA)
29
+ expect(dataFrame?.flags).toBe(0)
30
+ expect(dataFrame?.streamId).toBe(1)
31
+ expect(dataFrame?.sequence).toBe(3)
32
+ if (!dataFrame) throw new Error("expected a decoded v3 DATA frame")
33
+ expect(Buffer.from(dataFrame.payload).toString("hex")).toBe(Buffer.from("abc").toString("hex"))
34
+ })
26
35
 
27
- test("v3 framing rejects control/data stream confusion and oversized payloads", () => {
28
- assert.throws(
29
- () => encodeV3Frame({type: V3FrameType.DATA, streamId: 0, sequence: 1}, Buffer.from("x")),
30
- /stream ID zero/i
31
- )
32
- assert.throws(
33
- () => encodeV3Frame({type: V3FrameType.AUTH, streamId: 1, sequence: 0}, Buffer.from("x")),
34
- /connection control/i
35
- )
36
- assert.throws(() => encodeV3Frame({type: V3FrameType.PING, streamId: 0, sequence: 0, flags: 1}), /flags/i)
36
+ it("v3 framing rejects control/data stream confusion and oversized payloads", () => {
37
+ expect(() => encodeV3Frame({type: V3FrameType.DATA, streamId: 0, sequence: 1}, Buffer.from("x"))).toThrow(/stream ID zero/i)
38
+ expect(() => encodeV3Frame({type: V3FrameType.AUTH, streamId: 1, sequence: 0}, Buffer.from("x"))).toThrow(/connection control/i)
39
+ expect(() => encodeV3Frame({type: V3FrameType.PING, streamId: 0, sequence: 0, flags: 1})).toThrow(/flags/i)
37
40
 
38
- const decoder = new V3FrameDecoder({maxFrameBytes: 2, maxBufferedBytes: 64})
39
- assert.throws(
40
- () => decoder.push(encodeV3Frame({type: V3FrameType.DATA, streamId: 1, sequence: 3}, Buffer.from("abc"))),
41
- /payload length/i
42
- )
43
- })
41
+ const decoder = new V3FrameDecoder({maxFrameBytes: 2, maxBufferedBytes: 64})
42
+ expect(() => decoder.push(encodeV3Frame({type: V3FrameType.DATA, streamId: 1, sequence: 3}, Buffer.from("abc")))).toThrow(/payload length/i)
43
+ })
44
44
 
45
- test("v3 control payloads are bounded strict JSON objects", () => {
46
- const encoded = encodeV3Control({token: "secret", versions: [3]}, 128)
47
- assert.deepEqual(decodeV3Control(encoded, 128), {token: "secret", versions: [3]})
48
- assert.throws(() => encodeV3Control(["not-an-object"], 128), /object/i)
49
- assert.throws(() => decodeV3Control(Buffer.from("[]"), 128), /object/i)
50
- assert.throws(() => encodeV3Control({padding: "x".repeat(129)}, 128), /limit/i)
51
- })
45
+ it("v3 control payloads are bounded strict JSON objects", () => {
46
+ const encoded = encodeV3Control({token: "secret", versions: [3]}, 128)
47
+ expect(decodeV3Control(encoded, 128)).toEqual({token: "secret", versions: [3]})
48
+ expect(() => encodeV3Control(["not-an-object"], 128)).toThrow(/object/i)
49
+ expect(() => decodeV3Control(Buffer.from("[]"), 128)).toThrow(/object/i)
50
+ expect(() => encodeV3Control({padding: "x".repeat(129)}, 128)).toThrow(/limit/i)
51
+ })
52
52
 
53
- test("v3 reserves one negotiated compression flag for DATA only", () => {
54
- const wire = encodeV3Frame({type: V3FrameType.DATA, streamId: 2, sequence: 10, flags: V3FrameFlag.DATA_COMPRESSED}, Buffer.from("payload"))
55
- assert.equal(new V3FrameDecoder().push(wire)[0]?.flags, V3FrameFlag.DATA_COMPRESSED)
56
- assert.throws(() => encodeV3Frame({type: V3FrameType.GATEWAY_REGISTER, streamId: 0, sequence: 1, flags: V3FrameFlag.DATA_COMPRESSED}), /flags/i)
53
+ it("v3 reserves one negotiated compression flag for DATA only", () => {
54
+ const wire = encodeV3Frame({type: V3FrameType.DATA, streamId: 2, sequence: 10, flags: V3FrameFlag.DATA_COMPRESSED}, Buffer.from("payload"))
55
+ expect(new V3FrameDecoder().push(wire)[0]?.flags).toBe(V3FrameFlag.DATA_COMPRESSED)
56
+ expect(() => encodeV3Frame({type: V3FrameType.GATEWAY_REGISTER, streamId: 0, sequence: 1, flags: V3FrameFlag.DATA_COMPRESSED})).toThrow(/flags/i)
57
+ })
57
58
  })
@@ -1,21 +1,24 @@
1
1
  // @ts-check
2
2
 
3
- import assert from "node:assert/strict"
3
+ import {describe, expect, it} from "@velocious/testing"
4
4
  import {readFile} from "node:fs/promises"
5
5
  import {fileURLToPath} from "node:url"
6
- import test from "node:test"
7
6
 
8
7
  const packageJsonPath = fileURLToPath(new URL("../../package.json", import.meta.url))
9
8
 
10
- test("patch releases use release-patch", async () => {
11
- const packageJson = /** @type {{devDependencies: Record<string, string>, private?: boolean, scripts: Record<string, string>}} */ (JSON.parse(await readFile(packageJsonPath, "utf8")))
9
+ describe("release configuration", () => {
10
+ it("patch releases use release-patch", async () => {
11
+ const packageJson = /** @type {{devDependencies: Record<string, string>, private?: boolean, scripts: Record<string, string>}} */ (JSON.parse(await readFile(packageJsonPath, "utf8")))
12
12
 
13
- assert.equal(packageJson.private, undefined)
14
- assert.equal(packageJson.devDependencies["release-patch"], "^1.0.8")
15
- const check = packageJson.scripts.check
16
- const releasePatch = packageJson.scripts["release:patch"]
17
- assert.ok(check)
18
- assert.ok(releasePatch)
19
- assert.deepEqual(check.split(" && "), ["npm run lint", "npm run typecheck", "npm test"])
20
- assert.deepEqual(releasePatch.split(" && "), ["npm run check", "release-patch"])
13
+ expect(packageJson.private).toBe(undefined)
14
+ expect(packageJson.devDependencies["release-patch"]).toBe("^1.0.8")
15
+ const check = packageJson.scripts.check
16
+ const releasePatch = packageJson.scripts["release:patch"]
17
+ expect(check).toBeTruthy()
18
+ expect(releasePatch).toBeTruthy()
19
+ if (check === undefined) throw new Error("Expected a check script")
20
+ if (releasePatch === undefined) throw new Error("Expected a release:patch script")
21
+ expect(check.split(" && ")).toEqual(["npm run lint", "npm run typecheck", "npm test"])
22
+ expect(releasePatch.split(" && ")).toEqual(["npm run check", "release-patch"])
23
+ })
21
24
  })
@@ -1,6 +1,5 @@
1
- import assert from "node:assert/strict"
1
+ import {describe, expect, it} from "@velocious/testing"
2
2
  import {createHmac} from "node:crypto"
3
- import test from "node:test"
4
3
 
5
4
  import {
6
5
  attachDisconnectAttribution,
@@ -10,73 +9,75 @@ import {
10
9
  sanitizePeerControlDetail
11
10
  } from "../../../src/reverse/disconnect-attribution.js"
12
11
 
13
- test("disconnect attribution uses one bounded incident and named generations", () => {
14
- const attribution = createDisconnectAttribution({
15
- disconnectReason: "remote_socket_error",
16
- incidentOrigin: "reverse_hub",
17
- physicalRole: "gateway",
18
- clientConnectionGeneration: 17,
19
- gatewayConnectionGeneration: 23,
20
- fanOutCount: 5,
21
- resumeSuccessCount: 2,
22
- terminalStreamCount: 1
23
- })
24
-
25
- assert.match(attribution.disconnectIncidentId, /^[A-Za-z0-9_-]{22}$/)
26
- assert.deepEqual(parseDisconnectAttribution(attribution), attribution)
27
- assert.equal("generation" in attribution, false)
12
+ describe("disconnect attribution", () => {
13
+ it("disconnect attribution uses one bounded incident and named generations", () => {
14
+ const attribution = createDisconnectAttribution({
15
+ disconnectReason: "remote_socket_error",
16
+ incidentOrigin: "reverse_hub",
17
+ physicalRole: "gateway",
18
+ clientConnectionGeneration: 17,
19
+ gatewayConnectionGeneration: 23,
20
+ fanOutCount: 5,
21
+ resumeSuccessCount: 2,
22
+ terminalStreamCount: 1
23
+ })
28
24
 
29
- const error = /** @type {Error & Record<string, unknown>} */ (attachDisconnectAttribution(Object.assign(new Error("classified terminal"), {code: "GATEWAY_DISCONNECTED"}), attribution))
30
- assert.equal(error.code, "GATEWAY_DISCONNECTED")
31
- assert.equal(error.disconnectReason, "remote_socket_error")
32
- assert.equal(error.disconnectIncidentId, attribution.disconnectIncidentId)
33
- assert.equal(error.gatewayConnectionGeneration, 23)
34
- })
25
+ expect(attribution.disconnectIncidentId).toMatch(/^[A-Za-z0-9_-]{22}$/)
26
+ expect(parseDisconnectAttribution(attribution)).toEqual(attribution)
27
+ expect("generation" in attribution).toBe(false)
35
28
 
36
- test("peer control detail cannot assert hub-owned disconnect fields", () => {
37
- const result = sanitizePeerControlDetail({
38
- code: "TARGET_CLOSED",
39
- disconnectIncidentId: "AAAAAAAAAAAAAAAAAAAAAA",
40
- disconnectReason: "hub_shutdown",
41
- incidentOrigin: "reverse_hub",
42
- physicalRole: "gateway",
43
- gatewayConnectionGeneration: 9,
44
- fanOutCount: 4,
45
- target: "docker.internal:2375",
46
- token: "secret"
29
+ const error = /** @type {Error & Record<string, unknown>} */ (attachDisconnectAttribution(Object.assign(new Error("classified terminal"), {code: "GATEWAY_DISCONNECTED"}), attribution))
30
+ expect(error.code).toBe("GATEWAY_DISCONNECTED")
31
+ expect(error.disconnectReason).toBe("remote_socket_error")
32
+ expect(error.disconnectIncidentId).toBe(attribution.disconnectIncidentId)
33
+ expect(error.gatewayConnectionGeneration).toBe(23)
47
34
  })
48
35
 
49
- assert.deepEqual(result.detail, {code: "TARGET_CLOSED"})
50
- assert.equal(result.reservedFieldsStripped, 6)
51
- })
36
+ it("peer control detail cannot assert hub-owned disconnect fields", () => {
37
+ const result = sanitizePeerControlDetail({
38
+ code: "TARGET_CLOSED",
39
+ disconnectIncidentId: "AAAAAAAAAAAAAAAAAAAAAA",
40
+ disconnectReason: "hub_shutdown",
41
+ incidentOrigin: "reverse_hub",
42
+ physicalRole: "gateway",
43
+ gatewayConnectionGeneration: 9,
44
+ fanOutCount: 4,
45
+ target: "docker.internal:2375",
46
+ token: "secret"
47
+ })
52
48
 
53
- test("hub connection fingerprints are keyed, role-separated, and truncated to 128 bits", () => {
54
- const key = Buffer.alloc(32, 0x5a)
55
- const connectionId = "low_entropy_connection_01"
56
- const expected = createHmac("sha256", key)
57
- .update("socketduct-connection-id:v1")
58
- .update(Buffer.from([0]))
59
- .update("gateway")
60
- .update(Buffer.from([0]))
61
- .update(connectionId)
62
- .digest()
63
- .subarray(0, 16)
64
- .toString("hex")
49
+ expect(result.detail).toEqual({code: "TARGET_CLOSED"})
50
+ expect(result.reservedFieldsStripped).toBe(6)
51
+ })
65
52
 
66
- const actual = connectionFingerprint(key, "gateway", connectionId)
67
- assert.equal(actual, expected)
68
- assert.match(actual, /^[a-f0-9]{32}$/)
69
- assert.notEqual(actual, connectionId)
70
- assert.notEqual(actual, connectionFingerprint(key, "client", connectionId))
71
- assert.throws(() => connectionFingerprint(Buffer.alloc(31), "gateway", connectionId), /at least 32 bytes/)
72
- })
53
+ it("hub connection fingerprints are keyed, role-separated, and truncated to 128 bits", () => {
54
+ const key = Buffer.alloc(32, 0x5a)
55
+ const connectionId = "low_entropy_connection_01"
56
+ const expected = createHmac("sha256", key)
57
+ .update("socketduct-connection-id:v1")
58
+ .update(Buffer.from([0]))
59
+ .update("gateway")
60
+ .update(Buffer.from([0]))
61
+ .update(connectionId)
62
+ .digest()
63
+ .subarray(0, 16)
64
+ .toString("hex")
65
+
66
+ const actual = connectionFingerprint(key, "gateway", connectionId)
67
+ expect(actual).toBe(expected)
68
+ expect(actual).toMatch(/^[a-f0-9]{32}$/)
69
+ expect(actual).not.toBe(connectionId)
70
+ expect(actual).not.toBe(connectionFingerprint(key, "client", connectionId))
71
+ expect(() => connectionFingerprint(Buffer.alloc(31), "gateway", connectionId)).toThrow(/at least 32 bytes/)
72
+ })
73
73
 
74
- test("malformed attribution is rejected instead of partially trusted", () => {
75
- assert.equal(parseDisconnectAttribution({
76
- disconnectIncidentId: "AAAAAAAAAAAAAAAAAAAAAA",
77
- disconnectReason: "not-a-reason",
78
- incidentOrigin: "reverse_hub",
79
- physicalRole: "gateway",
80
- gatewayConnectionGeneration: 1
81
- }), undefined)
74
+ it("malformed attribution is rejected instead of partially trusted", () => {
75
+ expect(parseDisconnectAttribution({
76
+ disconnectIncidentId: "AAAAAAAAAAAAAAAAAAAAAA",
77
+ disconnectReason: "not-a-reason",
78
+ incidentOrigin: "reverse_hub",
79
+ physicalRole: "gateway",
80
+ gatewayConnectionGeneration: 1
81
+ })).toBeUndefined()
82
+ })
82
83
  })
@@ -1,54 +1,57 @@
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 {insertBoundedTerminalOutcome, ownerMayMissTerminalDelivery} from "../../src/reverse/terminal-outcomes.js"
5
4
 
6
- test("a healthy attached physical does not retain a terminal outcome; absent, closing, and pending-resume states do", () => {
7
- // Healthy attached: the peer receives the terminal frame and retires the
8
- // stream locally, so it never re-presents it. Retaining here is the
9
- // unbounded-growth bug: it must be a no-eligibility (no retention).
10
- assert.equal(ownerMayMissTerminalDelivery({closing: false, resumeRequired: false}), false)
11
- // Detached (no physical): the terminal frame cannot be delivered.
12
- assert.equal(ownerMayMissTerminalDelivery(undefined), true)
13
- // Closing: the frame may not be flushed.
14
- assert.equal(ownerMayMissTerminalDelivery({closing: true, resumeRequired: false}), true)
15
- // Paired-resume / pending-manifest state: the frame is answered through the
16
- // RESUME manifest instead of a live relay.
17
- assert.equal(ownerMayMissTerminalDelivery({closing: false, resumeRequired: true}), true)
18
- })
19
-
20
- test("retention is capped at the owner stream bound, evicting the oldest first in deterministic insertion order", () => {
21
- const outcomes = new Map()
22
- const limit = 3
23
- const outcome = (/** @type {string} */ code) => ({code, attribution: undefined})
24
-
25
- // Within the bound: every outcome is retained, in insertion order.
26
- insertBoundedTerminalOutcome(outcomes, "s1\0a", outcome("RESUME_STREAM_OMITTED"), limit)
27
- insertBoundedTerminalOutcome(outcomes, "s2\0b", outcome("BUFFER_LIMIT"), limit)
28
- assert.equal(outcomes.size, 2)
29
- assert.deepEqual([...outcomes.keys()], ["s1\0a", "s2\0b"])
30
-
31
- // Exactly at the bound: no eviction yet.
32
- insertBoundedTerminalOutcome(outcomes, "s3\0c", outcome("TARGET_CLOSED"), limit)
33
- assert.equal(outcomes.size, 3)
34
- assert.equal(outcomes.get("s1\0a")?.code, "RESUME_STREAM_OMITTED")
35
-
36
- // One past the bound: the oldest (s1) is evicted, the newest (s3) is kept.
37
- insertBoundedTerminalOutcome(outcomes, "s4\0d", outcome("RESUME_SESSION_LOST"), limit)
38
- assert.equal(outcomes.size, 3)
39
- assert.equal(outcomes.has("s1\0a"), false, "the oldest outcome must be evicted (fail-closed to RESUME_SESSION_LOST)")
40
- assert.deepEqual([...outcomes.keys()], ["s2\0b", "s3\0c", "s4\0d"])
41
- assert.equal(outcomes.get("s4\0d")?.code, "RESUME_SESSION_LOST", "the newest causal code must be retained")
42
-
43
- // Further churn keeps the bound and evicts the next-oldest, deterministically.
44
- insertBoundedTerminalOutcome(outcomes, "s5\0e", outcome("GATEWAY_MAINTENANCE"), limit)
45
- assert.equal(outcomes.size, 3)
46
- assert.deepEqual([...outcomes.keys()], ["s3\0c", "s4\0d", "s5\0e"])
47
- assert.equal(outcomes.has("s2\0b"), false)
48
-
49
- // Re-inserting a retained key updates it in place without disturbing the bound.
50
- insertBoundedTerminalOutcome(outcomes, "s5\0e", outcome("BUFFER_LIMIT"), limit)
51
- assert.equal(outcomes.size, 3)
52
- assert.equal(outcomes.get("s5\0e")?.code, "BUFFER_LIMIT")
53
- assert.deepEqual([...outcomes.keys()], ["s3\0c", "s4\0d", "s5\0e"])
5
+ describe("reverse terminal outcomes", () => {
6
+ it("a healthy attached physical does not retain a terminal outcome; absent, closing, and pending-resume states do", () => {
7
+ // Healthy attached: the peer receives the terminal frame and retires the
8
+ // stream locally, so it never re-presents it. Retaining here is the
9
+ // unbounded-growth bug: it must be a no-eligibility (no retention).
10
+ expect(ownerMayMissTerminalDelivery({closing: false, resumeRequired: false})).toBe(false)
11
+ // Detached (no physical): the terminal frame cannot be delivered.
12
+ expect(ownerMayMissTerminalDelivery(undefined)).toBe(true)
13
+ // Closing: the frame may not be flushed.
14
+ expect(ownerMayMissTerminalDelivery({closing: true, resumeRequired: false})).toBe(true)
15
+ // Paired-resume / pending-manifest state: the frame is answered through the
16
+ // RESUME manifest instead of a live relay.
17
+ expect(ownerMayMissTerminalDelivery({closing: false, resumeRequired: true})).toBe(true)
18
+ })
19
+
20
+ it("retention is capped at the owner stream bound, evicting the oldest first in deterministic insertion order", () => {
21
+ const outcomes = new Map()
22
+ const limit = 3
23
+ const outcome = (/** @type {string} */ code) => ({code, attribution: undefined})
24
+
25
+ // Within the bound: every outcome is retained, in insertion order.
26
+ insertBoundedTerminalOutcome(outcomes, "s1\0a", outcome("RESUME_STREAM_OMITTED"), limit)
27
+ insertBoundedTerminalOutcome(outcomes, "s2\0b", outcome("BUFFER_LIMIT"), limit)
28
+ expect(outcomes.size).toBe(2)
29
+ expect([...outcomes.keys()]).toEqual(["s1\0a", "s2\0b"])
30
+
31
+ // Exactly at the bound: no eviction yet.
32
+ insertBoundedTerminalOutcome(outcomes, "s3\0c", outcome("TARGET_CLOSED"), limit)
33
+ expect(outcomes.size).toBe(3)
34
+ expect(outcomes.get("s1\0a")?.code).toBe("RESUME_STREAM_OMITTED")
35
+
36
+ // One past the bound: the oldest (s1) is evicted, the newest (s3) is kept.
37
+ insertBoundedTerminalOutcome(outcomes, "s4\0d", outcome("RESUME_SESSION_LOST"), limit)
38
+ expect(outcomes.size).toBe(3)
39
+ // the oldest outcome must be evicted (fail-closed to RESUME_SESSION_LOST)
40
+ expect(outcomes.has("s1\0a")).toBe(false)
41
+ expect([...outcomes.keys()]).toEqual(["s2\0b", "s3\0c", "s4\0d"])
42
+ // the newest causal code must be retained
43
+ expect(outcomes.get("s4\0d")?.code).toBe("RESUME_SESSION_LOST")
44
+
45
+ // Further churn keeps the bound and evicts the next-oldest, deterministically.
46
+ insertBoundedTerminalOutcome(outcomes, "s5\0e", outcome("GATEWAY_MAINTENANCE"), limit)
47
+ expect(outcomes.size).toBe(3)
48
+ expect([...outcomes.keys()]).toEqual(["s3\0c", "s4\0d", "s5\0e"])
49
+ expect(outcomes.has("s2\0b")).toBe(false)
50
+
51
+ // Re-inserting a retained key updates it in place without disturbing the bound.
52
+ insertBoundedTerminalOutcome(outcomes, "s5\0e", outcome("BUFFER_LIMIT"), limit)
53
+ expect(outcomes.size).toBe(3)
54
+ expect(outcomes.get("s5\0e")?.code).toBe("BUFFER_LIMIT")
55
+ expect([...outcomes.keys()]).toEqual(["s3\0c", "s4\0d", "s5\0e"])
56
+ })
54
57
  })