socketduct 0.1.25 → 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 (56) 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/logging/compressed-log-writer.js +1 -1
  7. package/src/logging/zstd-frame-codec.js +15 -1
  8. package/test/e2e/gateway-process.test.js +93 -92
  9. package/test/e2e/logger-shutdown-process.test.js +40 -37
  10. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  11. package/test/e2e/reverse-gateway-process.test.js +328 -318
  12. package/test/integration/initial-target-recovery.test.js +333 -334
  13. package/test/integration/logging-cli.test.js +121 -119
  14. package/test/integration/logging-compressed-recovery.test.js +96 -95
  15. package/test/integration/multiplexed-compatibility.test.js +178 -177
  16. package/test/integration/multiplexed-corrections.test.js +360 -359
  17. package/test/integration/multiplexed-failures.test.js +253 -251
  18. package/test/integration/multiplexed-transport-set.test.js +334 -322
  19. package/test/integration/multiplexed-transport.test.js +135 -134
  20. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  21. package/test/integration/reverse-gateway.test.js +2309 -2218
  22. package/test/integration/reverse-resume-continuity.test.js +219 -218
  23. package/test/integration/reverse-route-class.test.js +358 -341
  24. package/test/integration/socketduct-http-agent.test.js +314 -312
  25. package/test/integration/socketduct-https-agent.test.js +516 -485
  26. package/test/integration/stream-compression.test.js +862 -824
  27. package/test/integration/tcp-forwarding.test.js +2550 -2447
  28. package/test/integration/tls-forwarding.test.js +593 -592
  29. package/test/unit/auth/auth.test.js +43 -44
  30. package/test/unit/auth/static-token-store.test.js +9 -8
  31. package/test/unit/client/connect.test.js +41 -40
  32. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  33. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  34. package/test/unit/client/multiplexed-transport-set.test.js +830 -748
  35. package/test/unit/client/socketduct-socket.test.js +768 -771
  36. package/test/unit/client/virtual-socket.test.js +198 -168
  37. package/test/unit/config/docker-gateway-files.test.js +224 -224
  38. package/test/unit/config/environment.test.js +472 -492
  39. package/test/unit/index.test.js +53 -52
  40. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  41. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  42. package/test/unit/logging/log-retention.test.js +52 -47
  43. package/test/unit/logging/log-segment-index.test.js +84 -81
  44. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  45. package/test/unit/protocol/compression.test.js +254 -298
  46. package/test/unit/protocol/frame.test.js +280 -276
  47. package/test/unit/protocol/v3-compression.test.js +21 -23
  48. package/test/unit/protocol/v3-frame.test.js +42 -41
  49. package/test/unit/release-configuration.test.js +15 -12
  50. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  51. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  52. package/test/unit/server/control-handler.test.js +232 -231
  53. package/test/unit/server/primary.test.js +51 -49
  54. package/test/unit/server/relay-runtime.test.js +280 -294
  55. package/test/unit/sessions/session.test.js +48 -62
  56. package/test/unit/spool/spool-file.test.js +401 -385
package/AGENTS.md CHANGED
@@ -160,9 +160,9 @@ docs/
160
160
  ## Definition of done for implementation PRs
161
161
 
162
162
  - Add a release-note fragment for every user-visible change, bug fix, security fix, or protocol change under `changelog.d/YYYYMMDDHHMMSS-short-slug.md`. Keep it to one concise, user-facing sentence; do not edit generated release notes directly.
163
- - Tests added or updated.
163
+ - Tests added or updated — test modules declare `describe`/`it`/`expect` from `@velocious/testing`; `node:test`/`node:assert` are not used in test modules.
164
164
  - Relevant docs updated.
165
- - `npm test` passes.
165
+ - `npm test` passes (every `test/**/*.test.js` module runs through the `velocious-test` runner).
166
166
  - Lint/typecheck passes once configured.
167
167
  - Security-sensitive changes mention how credentials/tokens are protected.
168
168
  - No open proxy behavior introduced.
@@ -0,0 +1 @@
1
+ Diagnostic log frames now default to zstd level 9 instead of 19: zstd context memory scales steeply with the level (~1-2 MB per compressed batch at 9 versus ~90 MB at 19), and per-batch contexts accumulated into a multi-GB resident/peak-RSS ratchet in long-lived processes that no JS-level accounting or GC pressure sees.
@@ -35,12 +35,14 @@ npm run lint
35
35
  git diff --check
36
36
  ```
37
37
 
38
- Run the two focused modules sequentially with the published `@velocious/testing`
39
- runner. `npm test` is the CI entry point: it discovers untouched legacy modules
40
- for Node's runner and then invokes the owning runner separately for each migrated
41
- logger module. `npm test -- --list` prints both selections without running tests.
42
- Do not run the full suite or shards locally during a correction pass; CI owns
43
- that coverage. Test cleanup uses the package's supported FIFO `withTestCleanup`.
38
+ Run the focused modules sequentially with the published `@velocious/testing`
39
+ runner. `npm test` is the CI entry point: it discovers every module under
40
+ `test/**/*.test.js` and runs each one through the `velocious-test` runner in
41
+ sorted order. `npm test -- --list` prints the discovered modules without running
42
+ tests, and `npm test -- <path-or-args>` forwards arguments to a focused runner
43
+ invocation. Do not run the full suite or shards locally during a correction
44
+ pass; CI owns that coverage. Test cleanup uses the package's supported FIFO
45
+ `withTestCleanup`.
44
46
 
45
47
  The process tests create and remove only their own private home subdirectories;
46
48
  the canonical `/home/dev` bind persists across child-process exits. They reuse
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socketduct",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Resilient authenticated tunnel/proxy for keeping socket streams alive across flaky links.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -38,7 +38,7 @@
38
38
  "devDependencies": {
39
39
  "@eslint/js": "^10.0.1",
40
40
  "@types/node": "^24.0.0",
41
- "@velocious/testing": "0.0.10",
41
+ "@velocious/testing": "^0.0.18",
42
42
  "awaitery": "^1.0.21",
43
43
  "eslint": "^10.0.3",
44
44
  "eslint-plugin-jsdoc": "^62.7.1",
package/scripts/test.js CHANGED
@@ -1,24 +1,19 @@
1
1
  import {spawnSync} from "node:child_process"
2
2
  import {globSync} from "node:fs"
3
3
 
4
- const migrated = ["test/unit/logging/structured-logger.test.js", "test/integration/logging-recovery.test.js"]
4
+ const args = process.argv.slice(2)
5
5
  const files = globSync("test/**/*.test.js").sort()
6
- for (const path of migrated) {
7
- if (!files.includes(path)) throw new Error(`Missing migrated test module: ${path}`)
8
- }
9
- const legacy = files.filter((path) => !migrated.includes(path))
10
- if (legacy.length === 0) throw new Error("No legacy test modules discovered")
11
- const commands = [
12
- {runner: process.execPath, args: ["--test", ...legacy]},
13
- ...migrated.map((path) => ({runner: "velocious-test", args: [path]}))
14
- ]
6
+ if (files.length === 0) throw new Error("No test modules discovered")
15
7
 
16
- if (process.argv.length === 3 && process.argv[2] === "--list") {
17
- console.log(JSON.stringify({legacy, migrated, commands}, null, 2))
8
+ if (args.length === 1 && args[0] === "--list") {
9
+ console.log(JSON.stringify({files}, null, 2))
18
10
  } else {
19
- if (process.argv.length !== 2) throw new Error("Only --list is supported; use the owning runner for focused tests")
11
+ const commands = args.length === 0
12
+ ? files.map((path) => ({args: [path], label: path}))
13
+ : [{args, label: args.join(" ")}]
20
14
  for (const command of commands) {
21
- const result = spawnSync(command.runner, command.args, {stdio: "inherit"})
15
+ if (commands.length > 1) console.log(`\n=== ${command.label} ===`)
16
+ const result = spawnSync("velocious-test", command.args, {stdio: "inherit"})
22
17
  if (result.error) throw result.error
23
18
  if (result.status !== 0) {
24
19
  process.exitCode = result.status ?? 1
@@ -36,7 +36,7 @@ const NONCRITICAL_EVENTS = new Set(["runtime.status", "transport.connecting", "t
36
36
  * @property {number} [maxSegmentBytes] - Maximum compressed segment size.
37
37
  * @property {number} [maxQueueBytes] - Shared producer/worker allocation bound.
38
38
  * @property {number} [criticalReserveBytes] - Queue bytes reserved for critical events.
39
- * @property {number} [compressionLevel] - Explicit zstd level, default 19.
39
+ * @property {number} [compressionLevel] - Explicit zstd level, default 9. High levels (12+) allocate tens of MB of zstd context memory per compressed batch; see `DEFAULT_LOG_COMPRESSION_LEVEL`.
40
40
  * @property {number} [retentionDays] - Closed-segment maximum age, default 30 days.
41
41
  * @property {number} [retentionBytes] - Directory quota, default 4 GiB.
42
42
  * @property {{write(chunk: string): unknown}} [stderr] - Secret-free health reporting destination.
@@ -2,7 +2,21 @@
2
2
  import {promisify} from "node:util"
3
3
  import {constants, zstdCompress, zstdDecompress} from "node:zlib"
4
4
 
5
- export const DEFAULT_LOG_COMPRESSION_LEVEL = 19
5
+ /**
6
+ * Default zstd level for per-batch diagnostic log frames.
7
+ *
8
+ * Every batch is compressed as its own frame, and a fresh zstd context's
9
+ * memory scales steeply with the level: level 9 allocates ~1-2 MB per call,
10
+ * level 12 tens of MB, and level 19 roughly 90 MB. Long-lived processes flush
11
+ * log batches continuously, so a high level turns those per-call context
12
+ * allocations into a multi-GB resident/peak-RSS ratchet that no JS-level
13
+ * accounting sees (the memory is freed by the compressor but retained by the
14
+ * allocator while the producing worker's isolate never pressures GC). JSONL
15
+ * diagnostics already compress 50x+ at level 9, and level 19 buys only ~15%
16
+ * smaller frames at ~20x the CPU and ~50x the per-call memory, so the default
17
+ * stays in the bounded-memory range. Raise it per process only deliberately.
18
+ */
19
+ export const DEFAULT_LOG_COMPRESSION_LEVEL = 9
6
20
 
7
21
  const compress = promisify(zstdCompress)
8
22
  const decompress = promisify(zstdDecompress)
@@ -1,10 +1,9 @@
1
- import assert from "node:assert/strict"
1
+ import {describe, expect, it, withTestCleanup} from "@velocious/testing"
2
2
  import {spawn} from "node:child_process"
3
3
  import {mkdtemp, rm} from "node:fs/promises"
4
4
  import {createConnection, createServer} from "node:net"
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 {startTcpEdge} from "../../src/edge/tcp-edge.js"
10
9
  import {encodeFrame, FrameDecoder} from "../../src/protocol/frame.js"
@@ -159,104 +158,106 @@ async function stopProcess(child) {
159
158
  }), PROCESS_TIMEOUT_MS, "Timed out waiting for relay shutdown")
160
159
  }
161
160
 
162
- test("e2e process gateway forwards edge traffic to an allowlisted target", async () => {
163
- const tempDir = await mkdtemp(join(tmpdir(), "socketduct-e2e-spool-"))
164
- const targetServer = createServer((socket) => {
165
- socket.on("data", (chunk) => socket.write(Buffer.concat([Buffer.from("echo:"), chunk])))
166
- })
167
- const targetPort = await listen(targetServer)
168
- const relayPort = await reservePort()
169
- const relay = await startRelayProcess({
170
- SOCKETDUCT_MODE: "relay",
171
- SOCKETDUCT_RELAY_HOST: "127.0.0.1",
172
- SOCKETDUCT_RELAY_PORT: String(relayPort),
173
- SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:e2e-token-not-placeholder",
174
- SOCKETDUCT_TARGET_ALLOWLIST: `127.0.0.1:${targetPort}`
175
- })
176
-
177
- const edge = await startTcpEdge({
178
- relayHost: "127.0.0.1",
179
- relayPort,
180
- token: "e2e-token-not-placeholder",
181
- sessionName: "e2e process gateway",
182
- target: {host: "127.0.0.1", port: targetPort},
183
- spool: new SessionSpool({rootDir: tempDir}),
184
- reconnectDelayMs: 10
185
- })
186
-
187
- test.after(async () => {
188
- await edge.close()
189
- await stopProcess(relay.process)
190
- await closeServer(targetServer)
191
- await rm(tempDir, {recursive: true, force: true})
192
- })
193
-
194
- const appSocket = createConnection({host: "127.0.0.1", port: edge.port})
195
- await waitConnect(appSocket)
196
- appSocket.write("hello-tensorbuzz")
197
- assert.equal((await readOnce(appSocket)).toString("utf8"), "echo:hello-tensorbuzz")
198
- appSocket.destroy()
199
- })
161
+ describe("e2e process gateway", () => {
162
+ it("e2e process gateway forwards edge traffic to an allowlisted target", async () => withTestCleanup(async (after) => {
163
+ const tempDir = await mkdtemp(join(tmpdir(), "socketduct-e2e-spool-"))
164
+ const targetServer = createServer((socket) => {
165
+ socket.on("data", (chunk) => socket.write(Buffer.concat([Buffer.from("echo:"), chunk])))
166
+ })
167
+ const targetPort = await listen(targetServer)
168
+ const relayPort = await reservePort()
169
+ const relay = await startRelayProcess({
170
+ SOCKETDUCT_MODE: "relay",
171
+ SOCKETDUCT_RELAY_HOST: "127.0.0.1",
172
+ SOCKETDUCT_RELAY_PORT: String(relayPort),
173
+ SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:e2e-token-not-placeholder",
174
+ SOCKETDUCT_TARGET_ALLOWLIST: `127.0.0.1:${targetPort}`
175
+ })
200
176
 
201
- test("e2e process gateway denies non-allowlisted target requests", async () => {
202
- const allowedTarget = createServer((socket) => {
203
- socket.on("data", (chunk) => socket.write(chunk))
204
- })
205
- const allowedPort = await listen(allowedTarget)
206
- const blockedPort = await reservePort()
207
- const relayPort = await reservePort()
208
- const relay = await startRelayProcess({
209
- SOCKETDUCT_MODE: "relay",
210
- SOCKETDUCT_RELAY_HOST: "127.0.0.1",
211
- SOCKETDUCT_RELAY_PORT: String(relayPort),
212
- SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:e2e-token-not-placeholder",
213
- SOCKETDUCT_TARGET_ALLOWLIST: `127.0.0.1:${allowedPort}`
214
- })
177
+ const edge = await startTcpEdge({
178
+ relayHost: "127.0.0.1",
179
+ relayPort,
180
+ token: "e2e-token-not-placeholder",
181
+ sessionName: "e2e process gateway",
182
+ target: {host: "127.0.0.1", port: targetPort},
183
+ spool: new SessionSpool({rootDir: tempDir}),
184
+ reconnectDelayMs: 10
185
+ })
215
186
 
216
- test.after(async () => {
217
- await stopProcess(relay.process)
218
- await closeServer(allowedTarget)
219
- })
187
+ after(async () => {
188
+ await edge.close()
189
+ await stopProcess(relay.process)
190
+ await closeServer(targetServer)
191
+ await rm(tempDir, {recursive: true, force: true})
192
+ })
220
193
 
221
- const tunnel = createConnection({host: "127.0.0.1", port: relayPort})
222
- const decoder = new FrameDecoder()
223
- await waitConnect(tunnel)
224
- tunnel.write(encodeFrame({
225
- type: "session.create",
226
- token: "e2e-token-not-placeholder",
227
- name: "blocked process gateway target",
228
- target: {host: "127.0.0.1", port: blockedPort},
229
- framing: [2]
194
+ const appSocket = createConnection({host: "127.0.0.1", port: edge.port})
195
+ await waitConnect(appSocket)
196
+ appSocket.write("hello-tensorbuzz")
197
+ expect((await readOnce(appSocket)).toString("utf8")).toBe("echo:hello-tensorbuzz")
198
+ appSocket.destroy()
230
199
  }))
231
200
 
232
- const [frame] = decoder.push(await readOnce(tunnel))
233
- assert.equal(frame?.header.type, "error")
234
- assert.equal(frame?.header.code, "TARGET_UNAUTHORIZED")
235
- tunnel.destroy()
236
- })
237
-
238
- test("e2e process gateway rejects placeholder tokens before listening", async () => {
239
- const relayPort = await reservePort()
240
- const child = spawn(process.execPath, ["src/index.js"], {
241
- cwd: process.cwd(),
242
- env: {
243
- ...process.env,
201
+ it("e2e process gateway denies non-allowlisted target requests", async () => withTestCleanup(async (after) => {
202
+ const allowedTarget = createServer((socket) => {
203
+ socket.on("data", (chunk) => socket.write(chunk))
204
+ })
205
+ const allowedPort = await listen(allowedTarget)
206
+ const blockedPort = await reservePort()
207
+ const relayPort = await reservePort()
208
+ const relay = await startRelayProcess({
244
209
  SOCKETDUCT_MODE: "relay",
245
210
  SOCKETDUCT_RELAY_HOST: "127.0.0.1",
246
211
  SOCKETDUCT_RELAY_PORT: String(relayPort),
247
- SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:replace-with-generated-token",
248
- SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:443"
249
- },
250
- stdio: ["ignore", "pipe", "pipe"]
251
- })
252
- let output = ""
253
- child.stdout.on("data", (chunk) => { output += chunk.toString("utf8") })
254
- child.stderr.on("data", (chunk) => { output += chunk.toString("utf8") })
212
+ SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:e2e-token-not-placeholder",
213
+ SOCKETDUCT_TARGET_ALLOWLIST: `127.0.0.1:${allowedPort}`
214
+ })
215
+
216
+ after(async () => {
217
+ await stopProcess(relay.process)
218
+ await closeServer(allowedTarget)
219
+ })
255
220
 
256
- const exitCode = await withTimeout(new Promise((resolve) => {
257
- child.once("exit", (code) => resolve(code))
258
- }), PROCESS_TIMEOUT_MS, "Timed out waiting for placeholder-token process exit")
221
+ const tunnel = createConnection({host: "127.0.0.1", port: relayPort})
222
+ const decoder = new FrameDecoder()
223
+ await waitConnect(tunnel)
224
+ tunnel.write(encodeFrame({
225
+ type: "session.create",
226
+ token: "e2e-token-not-placeholder",
227
+ name: "blocked process gateway target",
228
+ target: {host: "127.0.0.1", port: blockedPort},
229
+ framing: [2]
230
+ }))
231
+
232
+ const [frame] = decoder.push(await readOnce(tunnel))
233
+ expect(frame?.header.type).toBe("error")
234
+ expect(frame?.header.code).toBe("TARGET_UNAUTHORIZED")
235
+ tunnel.destroy()
236
+ }))
259
237
 
260
- assert.notEqual(exitCode, 0)
261
- assert.match(output, /placeholder tokens/)
238
+ it("e2e process gateway rejects placeholder tokens before listening", async () => {
239
+ const relayPort = await reservePort()
240
+ const child = spawn(process.execPath, ["src/index.js"], {
241
+ cwd: process.cwd(),
242
+ env: {
243
+ ...process.env,
244
+ SOCKETDUCT_MODE: "relay",
245
+ SOCKETDUCT_RELAY_HOST: "127.0.0.1",
246
+ SOCKETDUCT_RELAY_PORT: String(relayPort),
247
+ SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:replace-with-generated-token",
248
+ SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:443"
249
+ },
250
+ stdio: ["ignore", "pipe", "pipe"]
251
+ })
252
+ let output = ""
253
+ child.stdout.on("data", (chunk) => { output += chunk.toString("utf8") })
254
+ child.stderr.on("data", (chunk) => { output += chunk.toString("utf8") })
255
+
256
+ const exitCode = await withTimeout(new Promise((resolve) => {
257
+ child.once("exit", (code) => resolve(code))
258
+ }), PROCESS_TIMEOUT_MS, "Timed out waiting for placeholder-token process exit")
259
+
260
+ expect(exitCode).not.toBe(0)
261
+ expect(output).toMatch(/placeholder tokens/)
262
+ })
262
263
  })
@@ -1,52 +1,53 @@
1
1
  /* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param, jsdoc/require-returns */
2
- import assert from "node:assert/strict"
2
+ import {describe, expect, it, withTestCleanup} from "@velocious/testing"
3
3
  import {spawn} from "node:child_process"
4
4
  import {mkdtemp, rm} from "node:fs/promises"
5
5
  import {createServer} from "node:net"
6
6
  import {tmpdir} from "node:os"
7
7
  import {join} from "node:path"
8
- import test from "node:test"
9
8
 
10
9
  import {readCompressedLogDirectory} from "../../src/logging/compressed-log-reader.js"
11
10
 
12
11
  const TIMEOUT_MS = 8_000
13
12
 
14
- test("SIGTERM durably flushes queued critical lifecycle records before exit", async (t) => {
15
- const root = await mkdtemp(join(tmpdir(), "socketduct-logger-shutdown-"))
16
- const directory = join(root, "logs")
17
- const port = await reservePort()
18
- const child = spawn(process.execPath, ["src/index.js"], {
19
- cwd: process.cwd(),
20
- env: {
21
- ...process.env,
22
- SOCKETDUCT_MODE: "relay",
23
- SOCKETDUCT_RELAY_HOST: "127.0.0.1",
24
- SOCKETDUCT_RELAY_PORT: String(port),
25
- SOCKETDUCT_AUTH_TOKENS: "shutdown-test:shutdown-test-token",
26
- SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:1",
27
- SOCKETDUCT_STATUS_INTERVAL_MS: "0",
28
- SOCKETDUCT_LOG_DIRECTORY: directory,
29
- SOCKETDUCT_LOG_FLUSH_INTERVAL_MS: "60000",
30
- SOCKETDUCT_LOG_CRITICAL_FLUSH_INTERVAL_MS: "60000"
31
- },
32
- stdio: ["ignore", "pipe", "pipe"]
33
- })
34
- let output = ""
35
- child.stdout.on("data", (chunk) => { output += chunk.toString() })
36
- child.stderr.on("data", (chunk) => { output += chunk.toString() })
37
- t.after(async () => {
38
- if (child.exitCode === null) child.kill("SIGKILL")
39
- await rm(root, {recursive: true, force: true})
40
- })
13
+ describe("logger shutdown", () => {
14
+ it("SIGTERM durably flushes queued critical lifecycle records before exit", async () => withTestCleanup(async (after) => {
15
+ const root = await mkdtemp(join(tmpdir(), "socketduct-logger-shutdown-"))
16
+ const directory = join(root, "logs")
17
+ const port = await reservePort()
18
+ const child = spawn(process.execPath, ["src/index.js"], {
19
+ cwd: process.cwd(),
20
+ env: {
21
+ ...process.env,
22
+ SOCKETDUCT_MODE: "relay",
23
+ SOCKETDUCT_RELAY_HOST: "127.0.0.1",
24
+ SOCKETDUCT_RELAY_PORT: String(port),
25
+ SOCKETDUCT_AUTH_TOKENS: "shutdown-test:shutdown-test-token",
26
+ SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:1",
27
+ SOCKETDUCT_STATUS_INTERVAL_MS: "0",
28
+ SOCKETDUCT_LOG_DIRECTORY: directory,
29
+ SOCKETDUCT_LOG_FLUSH_INTERVAL_MS: "60000",
30
+ SOCKETDUCT_LOG_CRITICAL_FLUSH_INTERVAL_MS: "60000"
31
+ },
32
+ stdio: ["ignore", "pipe", "pipe"]
33
+ })
34
+ let output = ""
35
+ child.stdout.on("data", (chunk) => { output += chunk.toString() })
36
+ child.stderr.on("data", (chunk) => { output += chunk.toString() })
37
+ after(async () => {
38
+ if (child.exitCode === null) child.kill("SIGKILL")
39
+ await rm(root, {recursive: true, force: true})
40
+ })
41
41
 
42
- await waitUntil(() => output.includes('"event":"relay.started"'), child, () => output)
43
- assert.equal(child.kill("SIGTERM"), true)
44
- const exit = await withTimeout(new Promise((resolve) => child.once("exit", (code, signal) => resolve({code, signal}))), "process exit")
45
- assert.deepEqual(exit, {code: 0, signal: null})
42
+ await waitUntil(() => output.includes('"event":"relay.started"'), child, () => output)
43
+ expect(child.kill("SIGTERM")).toBe(true)
44
+ const exit = await withTimeout(new Promise((resolve) => child.once("exit", (code, signal) => resolve({code, signal}))), "process exit")
45
+ expect(exit).toEqual({code: 0, signal: null})
46
46
 
47
- const result = await readCompressedLogDirectory(directory)
48
- assert.equal(result.incomplete, false)
49
- assert.deepEqual(result.records.map((record) => record.event), ["process.started", "process.stopped", "log_batch_accepted", "log_frame_committed", "log_closed"])
47
+ const result = await readCompressedLogDirectory(directory)
48
+ expect(result.incomplete).toBe(false)
49
+ expect(result.records.map((record) => record.event)).toEqual(["process.started", "process.stopped", "log_batch_accepted", "log_frame_committed", "log_closed"])
50
+ }))
50
51
  })
51
52
 
52
53
  /** @returns {Promise<number>} A released loopback port. */
@@ -57,7 +58,9 @@ async function reservePort() {
57
58
  server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(undefined) })
58
59
  })
59
60
  const address = server.address()
60
- assert(address !== null && typeof address !== "string")
61
+ if (address === null || typeof address === "string") {
62
+ throw new Error("Server did not bind to a TCP port")
63
+ }
61
64
  await new Promise((resolve) => server.close(() => resolve(undefined)))
62
65
  return address.port
63
66
  }