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,63 +1,86 @@
1
1
  /* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param-type, jsdoc/require-returns */
2
- import assert from "node:assert/strict"
3
- import {readlinkSync} from "node:fs"
2
+ import {describe, expect, it, withTestCleanup} from "@velocious/testing"
3
+ import {chmodSync, chownSync, readlinkSync} from "node:fs"
4
4
  import {mkdtemp, readFile, readdir, rm, utimes, writeFile} from "node:fs/promises"
5
5
  import {tmpdir} from "node:os"
6
6
  import {join} from "node:path"
7
- import test from "node:test"
8
7
 
9
8
  import {enforceLogRetention} from "../../../src/logging/log-retention.js"
10
9
 
11
- test("retention removes only oldest sealed segments and preserves active evidence", async (t) => {
12
- const directory = await mkdtemp(join(tmpdir(), "socketduct-retention-"))
13
- t.after(() => rm(directory, {recursive: true, force: true}))
14
- const old = await createSegment(directory, "reverse-hub-AAAAAAAAAAAAAAAAAAAAAA-123e4567-e89b-42d3-a456-426614174000", 700, true)
15
- const newer = await createSegment(directory, "reverse-hub-BBBBBBBBBBBBBBBBBBBBBB-123e4567-e89b-42d3-a456-426614174001", 700, true)
16
- const active = await createSegment(directory, "reverse-hub-CCCCCCCCCCCCCCCCCCCCCC-123e4567-e89b-42d3-a456-426614174002", 700, false)
17
- await utimes(old.data, new Date(1_000), new Date(1_000))
18
- await utimes(newer.data, new Date(2_000), new Date(2_000))
19
- await utimes(active.data, new Date(3_000), new Date(3_000))
10
+ describe("log retention", () => {
11
+ it("retention removes only oldest sealed segments and preserves active evidence", async () => withTestCleanup(async (after) => {
12
+ const directory = await mkdtemp(join(tmpdir(), "socketduct-retention-"))
13
+ after(() => rm(directory, {recursive: true, force: true}))
14
+ const old = await createSegment(directory, "reverse-hub-AAAAAAAAAAAAAAAAAAAAAA-123e4567-e89b-42d3-a456-426614174000", 700, true)
15
+ const newer = await createSegment(directory, "reverse-hub-BBBBBBBBBBBBBBBBBBBBBB-123e4567-e89b-42d3-a456-426614174001", 700, true)
16
+ const active = await createSegment(directory, "reverse-hub-CCCCCCCCCCCCCCCCCCCCCC-123e4567-e89b-42d3-a456-426614174002", 700, false)
17
+ await utimes(old.data, new Date(1_000), new Date(1_000))
18
+ await utimes(newer.data, new Date(2_000), new Date(2_000))
19
+ await utimes(active.data, new Date(3_000), new Date(3_000))
20
20
 
21
- const result = await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1800, now: () => new Date(3_000)})
22
- const names = await readdir(directory)
23
- assert.equal(names.some((name) => name.startsWith(old.name)), false)
24
- assert(names.some((name) => name.startsWith(newer.name)))
25
- assert(names.some((name) => name.startsWith(active.name)))
26
- assert.equal(result.deletedSegments, 1)
27
- })
21
+ const result = await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1800, now: () => new Date(3_000)})
22
+ const names = await readdir(directory)
23
+ expect(names.some((name) => name.startsWith(old.name))).toBe(false)
24
+ expect(names.some((name) => name.startsWith(newer.name))).toBeTruthy()
25
+ expect(names.some((name) => name.startsWith(active.name))).toBeTruthy()
26
+ expect(result.deletedSegments).toBe(1)
27
+ }))
28
28
 
29
- test("retention fails closed when active or quarantined bytes prevent quota compliance", async (t) => {
30
- const directory = await mkdtemp(join(tmpdir(), "socketduct-retention-blocked-"))
31
- t.after(() => rm(directory, {recursive: true, force: true}))
32
- const active = await createSegment(directory, "reverse-client-DDDDDDDDDDDDDDDDDDDDDD-123e4567-e89b-42d3-a456-426614174003", 1800, false)
33
- await writeFile(`${active.data}.quarantine`, "LOG_EVIDENCE_INVALID", {mode: 0o600})
29
+ it("retention fails closed when active or quarantined bytes prevent quota compliance", async () => withTestCleanup(async (after) => {
30
+ const directory = await mkdtemp(join(tmpdir(), "socketduct-retention-blocked-"))
31
+ after(() => rm(directory, {recursive: true, force: true}))
32
+ const active = await createSegment(directory, "reverse-client-DDDDDDDDDDDDDDDDDDDDDD-123e4567-e89b-42d3-a456-426614174003", 1800, false)
33
+ await writeFile(`${active.data}.quarantine`, "LOG_EVIDENCE_INVALID", {mode: 0o600})
34
34
 
35
- await assert.rejects(
36
- () => enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1024, now: () => new Date(3_000)}),
37
- (error) => error instanceof Error && /** @type {{code?: unknown}} */ (error).code === "LOG_RETENTION_QUOTA_BLOCKED"
38
- )
39
- assert((await readdir(directory)).some((name) => name.startsWith(active.name)))
40
- })
35
+ let error
36
+ try {
37
+ await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1024, now: () => new Date(3_000)})
38
+ } catch (caught) {
39
+ error = caught
40
+ }
41
+ expect(error).toBeDefined()
42
+ expect(error instanceof Error && /** @type {{code?: unknown}} */ (error).code === "LOG_RETENTION_QUOTA_BLOCKED").toBeTruthy()
43
+ expect((await readdir(directory)).some((name) => name.startsWith(active.name))).toBeTruthy()
44
+ }))
45
+
46
+ it("retention reclaims stale ownership without displacing a live process instance", async () => withTestCleanup(async (after) => {
47
+ const directory = await mkdtemp(join(tmpdir(), "socketduct-retention-owner-"))
48
+ after(() => rm(directory, {recursive: true, force: true}))
49
+ const lockPath = join(directory, ".socketduct-retention.lock")
50
+ const processStart = processStartIdentity(await readFile(`/proc/${process.pid}/stat`, "utf8"))
51
+ const tid = Number(readlinkSync("/proc/thread-self").split("/").at(-1))
52
+ const threadStart = processStartIdentity(await readFile(`/proc/${process.pid}/task/${tid}/stat`, "utf8"))
53
+ const staleTemporary = `.socketduct-retention-owner-${process.pid}-${BigInt(processStart) + 1n}-${tid}-${threadStart}-123e4567-e89b-42d3-a456-426614174099.tmp`
54
+ await writeFile(join(directory, staleTemporary), "incomplete", {mode: 0o600})
55
+
56
+ await writeFile(lockPath, `${JSON.stringify({version: 1, pid: process.pid, processStart: String(BigInt(processStart) + 1n), tid, threadStart})}\n`, {mode: 0o600})
57
+ expect((await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1024})).skipped).toBe(false)
58
+ expect((await readdir(directory)).includes(staleTemporary)).toBe(false)
59
+
60
+ await writeFile(lockPath, `${JSON.stringify({version: 1, pid: process.pid, processStart, tid: 2_147_483_647, threadStart: "1"})}\n`, {mode: 0o600})
61
+ expect((await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1024})).skipped).toBe(false)
62
+
63
+ await writeFile(lockPath, `${JSON.stringify({version: 1, pid: process.pid, processStart, tid, threadStart})}\n`, {mode: 0o600})
64
+ expect((await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1024})).skipped).toBe(true)
65
+ }))
41
66
 
42
- test("retention reclaims stale ownership without displacing a live process instance", async (t) => {
43
- const directory = await mkdtemp(join(tmpdir(), "socketduct-retention-owner-"))
44
- t.after(() => rm(directory, {recursive: true, force: true}))
45
- const lockPath = join(directory, ".socketduct-retention.lock")
46
- const processStart = processStartIdentity(await readFile(`/proc/${process.pid}/stat`, "utf8"))
47
- const tid = Number(readlinkSync("/proc/thread-self").split("/").at(-1))
48
- const threadStart = processStartIdentity(await readFile(`/proc/${process.pid}/task/${tid}/stat`, "utf8"))
49
- const staleTemporary = `.socketduct-retention-owner-${process.pid}-${BigInt(processStart) + 1n}-${tid}-${threadStart}-123e4567-e89b-42d3-a456-426614174099.tmp`
50
- await writeFile(join(directory, staleTemporary), "incomplete", {mode: 0o600})
67
+ it("retention reclaims a lock left behind by a privileged process it cannot read", async () => withTestCleanup(async (after) => {
68
+ const directory = await mkdtemp(join(tmpdir(), "socketduct-retention-foreign-"))
69
+ after(() => rm(directory, {recursive: true, force: true}))
70
+ const lockPath = join(directory, ".socketduct-retention.lock")
71
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined
51
72
 
52
- await writeFile(lockPath, `${JSON.stringify({version: 1, pid: process.pid, processStart: String(BigInt(processStart) + 1n), tid, threadStart})}\n`, {mode: 0o600})
53
- assert.equal((await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1024})).skipped, false)
54
- assert.equal((await readdir(directory)).includes(staleTemporary), false)
73
+ await writeFile(lockPath, `${JSON.stringify({version: 1, pid: 1, processStart: "1", tid: 1, threadStart: "1"})}\n`, {mode: 0o600})
55
74
 
56
- await writeFile(lockPath, `${JSON.stringify({version: 1, pid: process.pid, processStart, tid: 2_147_483_647, threadStart: "1"})}\n`, {mode: 0o600})
57
- assert.equal((await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1024})).skipped, false)
75
+ if (uid === 0) chownSync(lockPath, 65_534, 65_534)
76
+ else if (uid !== undefined) chmodSync(lockPath, 0o000)
58
77
 
59
- await writeFile(lockPath, `${JSON.stringify({version: 1, pid: process.pid, processStart, tid, threadStart})}\n`, {mode: 0o600})
60
- assert.equal((await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1024})).skipped, true)
78
+ // The lock owner record is unreadable for this process, so retention cannot
79
+ // wait for it: it must reclaim the file and complete instead of failing the
80
+ // durable log sink for the whole server process.
81
+ expect((await enforceLogRetention({directory, maxAgeMs: 60_000, maxBytes: 1024})).skipped).toBe(false)
82
+ expect((await readdir(directory)).includes(".socketduct-retention.lock")).toBe(false)
83
+ }))
61
84
  })
62
85
 
63
86
  /**
@@ -80,7 +103,7 @@ function processStartIdentity(value) {
80
103
  const close = value.lastIndexOf(")")
81
104
  const fields = value.slice(close + 2).trim().split(/\s+/)
82
105
  const start = fields[19]
83
- if (start === undefined) assert.fail("process stat omits start time")
84
- assert.match(start, /^[0-9]+$/)
106
+ if (start === undefined) throw new Error("process stat omits start time")
107
+ expect(start).toMatch(/^[0-9]+$/)
85
108
  return start
86
109
  }
@@ -1,6 +1,5 @@
1
- import assert from "node:assert/strict"
1
+ import {describe, expect, it} from "@velocious/testing"
2
2
  import {createHash, randomBytes} from "node:crypto"
3
- import test from "node:test"
4
3
 
5
4
  import {
6
5
  INDEX_ENTRY_BYTES,
@@ -12,93 +11,97 @@ import {
12
11
  uuidBytes
13
12
  } from "../../../src/logging/log-segment-index.js"
14
13
 
15
- test("index v1 header and entry round-trip in the exact fixed-width format", () => {
16
- const segmentUuid = uuidBytes("123e4567-e89b-42d3-a456-426614174000")
17
- const writerEpoch = randomBytes(16)
18
- const batchId = randomBytes(16)
19
- const frameHash = createHash("sha256").update("frame").digest()
20
- const header = createIndexHeader({segmentUuid, writerEpoch})
21
- const entry = createIndexEntry({
22
- segmentUuid,
23
- writerEpoch,
24
- batchId,
25
- firstSequence: 1n,
26
- lastSequence: 3n,
27
- dataOffset: 0n,
28
- compressedLength: 91n,
29
- uncompressedLength: 240n,
30
- recordCount: 3,
31
- minTimestamp: 1_795_000_000_000n,
32
- maxTimestamp: 1_795_000_000_010n,
33
- frameSha256: frameHash
34
- })
14
+ describe("log segment index", () => {
15
+ it("index v1 header and entry round-trip in the exact fixed-width format", () => {
16
+ const segmentUuid = uuidBytes("123e4567-e89b-42d3-a456-426614174000")
17
+ const writerEpoch = randomBytes(16)
18
+ const batchId = randomBytes(16)
19
+ const frameHash = createHash("sha256").update("frame").digest()
20
+ const header = createIndexHeader({segmentUuid, writerEpoch})
21
+ const entry = createIndexEntry({
22
+ segmentUuid,
23
+ writerEpoch,
24
+ batchId,
25
+ firstSequence: 1n,
26
+ lastSequence: 3n,
27
+ dataOffset: 0n,
28
+ compressedLength: 91n,
29
+ uncompressedLength: 240n,
30
+ recordCount: 3,
31
+ minTimestamp: 1_795_000_000_000n,
32
+ maxTimestamp: 1_795_000_000_010n,
33
+ frameSha256: frameHash
34
+ })
35
35
 
36
- assert.equal(header.length, INDEX_HEADER_BYTES)
37
- assert.equal(entry.length, INDEX_ENTRY_BYTES)
38
- assert.deepEqual(parseIndexHeader(header), {version: 1, segmentUuid, writerEpoch})
39
- assert.deepEqual(parseIndexEntry(entry, {segmentUuid, writerEpoch}), {
40
- version: 1,
41
- flags: 0,
42
- segmentUuid,
43
- writerEpoch,
44
- batchId,
45
- firstSequence: 1n,
46
- lastSequence: 3n,
47
- dataOffset: 0n,
48
- compressedLength: 91n,
49
- uncompressedLength: 240n,
50
- recordCount: 3,
51
- minTimestamp: 1_795_000_000_000n,
52
- maxTimestamp: 1_795_000_000_010n,
53
- frameSha256: frameHash
36
+ expect(header.length).toBe(INDEX_HEADER_BYTES)
37
+ expect(entry.length).toBe(INDEX_ENTRY_BYTES)
38
+ const parsedHeader = parseIndexHeader(header)
39
+ expect(parsedHeader.version).toBe(1)
40
+ expect(Buffer.from(parsedHeader.segmentUuid).equals(Buffer.from(segmentUuid))).toBe(true)
41
+ expect(Buffer.from(parsedHeader.writerEpoch).equals(Buffer.from(writerEpoch))).toBe(true)
42
+ const parsedEntry = parseIndexEntry(entry, {segmentUuid, writerEpoch})
43
+ expect(parsedEntry.version).toBe(1)
44
+ expect(parsedEntry.flags).toBe(0)
45
+ expect(Buffer.from(parsedEntry.segmentUuid).equals(Buffer.from(segmentUuid))).toBe(true)
46
+ expect(Buffer.from(parsedEntry.writerEpoch).equals(Buffer.from(writerEpoch))).toBe(true)
47
+ expect(Buffer.from(parsedEntry.batchId).equals(Buffer.from(batchId))).toBe(true)
48
+ expect(parsedEntry.firstSequence).toBe(1n)
49
+ expect(parsedEntry.lastSequence).toBe(3n)
50
+ expect(parsedEntry.dataOffset).toBe(0n)
51
+ expect(parsedEntry.compressedLength).toBe(91n)
52
+ expect(parsedEntry.uncompressedLength).toBe(240n)
53
+ expect(parsedEntry.recordCount).toBe(3)
54
+ expect(parsedEntry.minTimestamp).toBe(1_795_000_000_000n)
55
+ expect(parsedEntry.maxTimestamp).toBe(1_795_000_000_010n)
56
+ expect(Buffer.from(parsedEntry.frameSha256).equals(Buffer.from(frameHash))).toBe(true)
54
57
  })
55
- })
56
58
 
57
- test("index CRC and identity corruption fail closed", () => {
58
- const segmentUuid = randomBytes(16)
59
- const writerEpoch = randomBytes(16)
60
- const header = createIndexHeader({segmentUuid, writerEpoch})
61
- header[20] = /** @type {number} */ (header[20]) ^ 0xff
62
- assert.throws(() => parseIndexHeader(header), /CRC32C/)
59
+ it("index CRC and identity corruption fail closed", () => {
60
+ const segmentUuid = randomBytes(16)
61
+ const writerEpoch = randomBytes(16)
62
+ const header = createIndexHeader({segmentUuid, writerEpoch})
63
+ header[20] = /** @type {number} */ (header[20]) ^ 0xff
64
+ expect(() => parseIndexHeader(header)).toThrow(/CRC32C/)
63
65
 
64
- const entry = createIndexEntry({
65
- segmentUuid,
66
- writerEpoch,
67
- batchId: randomBytes(16),
68
- firstSequence: 8n,
69
- lastSequence: 8n,
70
- dataOffset: 10n,
71
- compressedLength: 20n,
72
- uncompressedLength: 30n,
73
- recordCount: 1,
74
- minTimestamp: 4n,
75
- maxTimestamp: 4n,
76
- frameSha256: randomBytes(32)
77
- })
78
- entry[151] = /** @type {number} */ (entry[151]) ^ 0xff
79
- assert.throws(() => parseIndexEntry(entry, {segmentUuid, writerEpoch}), /CRC32C/)
80
- })
81
-
82
- test("index bytes contain no searchable diagnostic dimensions", () => {
83
- const forbidden = ["minisforum-zyxel", "disconnect-incident", "GATEWAY_DISCONNECTED", "docker.internal"]
84
- const segmentUuid = randomBytes(16)
85
- const writerEpoch = randomBytes(16)
86
- const encoded = Buffer.concat([
87
- createIndexHeader({segmentUuid, writerEpoch}),
88
- createIndexEntry({
66
+ const entry = createIndexEntry({
89
67
  segmentUuid,
90
68
  writerEpoch,
91
69
  batchId: randomBytes(16),
92
- firstSequence: 1n,
93
- lastSequence: 1n,
94
- dataOffset: 0n,
95
- compressedLength: 1n,
96
- uncompressedLength: 1n,
70
+ firstSequence: 8n,
71
+ lastSequence: 8n,
72
+ dataOffset: 10n,
73
+ compressedLength: 20n,
74
+ uncompressedLength: 30n,
97
75
  recordCount: 1,
98
- minTimestamp: 1n,
99
- maxTimestamp: 1n,
76
+ minTimestamp: 4n,
77
+ maxTimestamp: 4n,
100
78
  frameSha256: randomBytes(32)
101
79
  })
102
- ])
103
- for (const value of forbidden) assert.equal(encoded.includes(Buffer.from(value)), false)
80
+ entry[151] = /** @type {number} */ (entry[151]) ^ 0xff
81
+ expect(() => parseIndexEntry(entry, {segmentUuid, writerEpoch})).toThrow(/CRC32C/)
82
+ })
83
+
84
+ it("index bytes contain no searchable diagnostic dimensions", () => {
85
+ const forbidden = ["minisforum-zyxel", "disconnect-incident", "GATEWAY_DISCONNECTED", "docker.internal"]
86
+ const segmentUuid = randomBytes(16)
87
+ const writerEpoch = randomBytes(16)
88
+ const encoded = Buffer.concat([
89
+ createIndexHeader({segmentUuid, writerEpoch}),
90
+ createIndexEntry({
91
+ segmentUuid,
92
+ writerEpoch,
93
+ batchId: randomBytes(16),
94
+ firstSequence: 1n,
95
+ lastSequence: 1n,
96
+ dataOffset: 0n,
97
+ compressedLength: 1n,
98
+ uncompressedLength: 1n,
99
+ recordCount: 1,
100
+ minTimestamp: 1n,
101
+ maxTimestamp: 1n,
102
+ frameSha256: randomBytes(32)
103
+ })
104
+ ])
105
+ for (const value of forbidden) expect(encoded.includes(Buffer.from(value))).toBe(false)
106
+ })
104
107
  })
@@ -1,24 +1,33 @@
1
- import assert from "node:assert/strict"
2
- import test from "node:test"
1
+ import {describe, expect, it} from "@velocious/testing"
3
2
 
4
- import {compressLogFrame, decompressLogFrame} from "../../../src/logging/zstd-frame-codec.js"
3
+ import {DEFAULT_LOG_COMPRESSION_LEVEL, compressLogFrame, decompressLogFrame} from "../../../src/logging/zstd-frame-codec.js"
5
4
 
6
- test("log codec emits independently decodable standard zstd frames with checksums at level 19", async () => {
7
- const firstRaw = Buffer.from('{"event":"first"}\n')
8
- const secondRaw = Buffer.from('{"event":"second"}\n')
9
- const first = await compressLogFrame(firstRaw)
10
- const second = await compressLogFrame(secondRaw)
5
+ describe("zstd log frame codec", () => {
6
+ it("emits independently decodable standard zstd frames with checksums at the default level", async () => {
7
+ const firstRaw = Buffer.from('{"event":"first"}\n')
8
+ const secondRaw = Buffer.from('{"event":"second"}\n')
9
+ const first = await compressLogFrame(firstRaw)
10
+ const second = await compressLogFrame(secondRaw)
11
11
 
12
- assert.deepEqual(first.subarray(0, 4), Buffer.from([0x28, 0xb5, 0x2f, 0xfd]))
13
- assert.notEqual(/** @type {number} */ (first[4]) & 0x04, 0, "zstd frame checksum flag must be set")
14
- assert.deepEqual(await decompressLogFrame(first), firstRaw)
15
- assert.deepEqual(await decompressLogFrame(second), secondRaw)
16
- assert.notDeepEqual(first, second)
17
- })
12
+ expect(first.subarray(0, 4).toString("hex")).toBe("28b52ffd")
13
+ expect(Number(first[4]) & 0x04).toBeGreaterThan(0)
14
+ expect((await decompressLogFrame(first)).toString("hex")).toBe(firstRaw.toString("hex"))
15
+ expect((await decompressLogFrame(second)).toString("hex")).toBe(secondRaw.toString("hex"))
16
+ expect(first.equals(second)).toBe(false)
17
+ })
18
+
19
+ it("never falls back to plaintext", async () => {
20
+ const raw = Buffer.from("small\n")
21
+ const frame = await compressLogFrame(raw)
22
+
23
+ expect(frame.equals(raw)).toBe(false)
24
+ await expect(decompressLogFrame(Buffer.from("small\n"))).rejects.toThrow()
25
+ })
18
26
 
19
- test("log codec never falls back to plaintext", async () => {
20
- const raw = Buffer.from("small\n")
21
- const frame = await compressLogFrame(raw)
22
- assert.equal(frame.equals(raw), false)
23
- await assert.rejects(() => decompressLogFrame(Buffer.from("small\n")))
27
+ it("default compression level stays memory-bounded", () => {
28
+ // A fresh zstd context per batch costs ~1-2 MB at level 9, tens of MB at 12
29
+ // and ~90 MB at 19. Keep the default in the bounded range unless a
30
+ // deliberate review accepts the resident-memory consequences.
31
+ expect(DEFAULT_LOG_COMPRESSION_LEVEL).toBeLessThanOrEqual(9)
32
+ })
24
33
  })