socketduct 0.1.25 → 0.1.27

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 (58) hide show
  1. package/AGENTS.md +2 -2
  2. package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
  3. package/changelog.d/20261025070000-reclaim-unreadable-retention-lock.md +14 -0
  4. package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
  5. package/package.json +2 -2
  6. package/scripts/test.js +9 -14
  7. package/src/logging/compressed-log-writer.js +1 -1
  8. package/src/logging/log-retention.js +36 -1
  9. package/src/logging/zstd-frame-codec.js +15 -1
  10. package/test/e2e/gateway-process.test.js +93 -92
  11. package/test/e2e/logger-shutdown-process.test.js +40 -37
  12. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  13. package/test/e2e/reverse-gateway-process.test.js +328 -318
  14. package/test/integration/initial-target-recovery.test.js +333 -334
  15. package/test/integration/logging-cli.test.js +121 -119
  16. package/test/integration/logging-compressed-recovery.test.js +96 -95
  17. package/test/integration/multiplexed-compatibility.test.js +178 -177
  18. package/test/integration/multiplexed-corrections.test.js +360 -359
  19. package/test/integration/multiplexed-failures.test.js +253 -251
  20. package/test/integration/multiplexed-transport-set.test.js +334 -322
  21. package/test/integration/multiplexed-transport.test.js +135 -134
  22. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  23. package/test/integration/reverse-gateway.test.js +2309 -2218
  24. package/test/integration/reverse-resume-continuity.test.js +219 -218
  25. package/test/integration/reverse-route-class.test.js +358 -341
  26. package/test/integration/socketduct-http-agent.test.js +314 -312
  27. package/test/integration/socketduct-https-agent.test.js +516 -485
  28. package/test/integration/stream-compression.test.js +862 -824
  29. package/test/integration/tcp-forwarding.test.js +2550 -2447
  30. package/test/integration/tls-forwarding.test.js +593 -592
  31. package/test/unit/auth/auth.test.js +43 -44
  32. package/test/unit/auth/static-token-store.test.js +9 -8
  33. package/test/unit/client/connect.test.js +41 -40
  34. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  35. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  36. package/test/unit/client/multiplexed-transport-set.test.js +830 -748
  37. package/test/unit/client/socketduct-socket.test.js +768 -771
  38. package/test/unit/client/virtual-socket.test.js +198 -168
  39. package/test/unit/config/docker-gateway-files.test.js +224 -224
  40. package/test/unit/config/environment.test.js +472 -492
  41. package/test/unit/index.test.js +53 -52
  42. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  43. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  44. package/test/unit/logging/log-retention.test.js +71 -48
  45. package/test/unit/logging/log-segment-index.test.js +84 -81
  46. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  47. package/test/unit/protocol/compression.test.js +254 -298
  48. package/test/unit/protocol/frame.test.js +280 -276
  49. package/test/unit/protocol/v3-compression.test.js +21 -23
  50. package/test/unit/protocol/v3-frame.test.js +42 -41
  51. package/test/unit/release-configuration.test.js +15 -12
  52. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  53. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  54. package/test/unit/server/control-handler.test.js +232 -231
  55. package/test/unit/server/primary.test.js +51 -49
  56. package/test/unit/server/relay-runtime.test.js +280 -294
  57. package/test/unit/sessions/session.test.js +48 -62
  58. 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
  })