rollbridge 0.1.49 → 0.1.55

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 (39) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +5 -0
  3. package/changelog.d/20260909120000-velocious-testing.md +1 -0
  4. package/docs/cli.md +7 -1
  5. package/docs/generation-deployment-contract.md +9 -0
  6. package/eslint.config.js +8 -0
  7. package/package.json +3 -2
  8. package/src/cli.js +10 -2
  9. package/src/daemon.js +102 -12
  10. package/src/process-guardian.js +5 -1
  11. package/src/release-group.js +48 -1
  12. package/test/completion.test.js +18 -16
  13. package/test/config-examples.test.js +16 -17
  14. package/test/config-path.test.js +10 -11
  15. package/test/config-validation.test.js +163 -167
  16. package/test/control-protocol.test.js +75 -14
  17. package/test/daemon-bootstrap.test.js +104 -104
  18. package/test/daemon-runtime.test.js +17 -26
  19. package/test/doctor.test.js +51 -49
  20. package/test/event-log.test.js +13 -11
  21. package/test/guardian-client.test.js +160 -145
  22. package/test/health.test.js +6 -4
  23. package/test/logs.test.js +23 -17
  24. package/test/managed-process.test.js +96 -91
  25. package/test/owner-recovery.test.js +254 -239
  26. package/test/owner-replacement.test.js +228 -223
  27. package/test/package-metadata.test.js +48 -39
  28. package/test/port-allocator.test.js +13 -16
  29. package/test/predeploy-cleanup.test.js +12 -10
  30. package/test/process-memory.test.js +17 -15
  31. package/test/proxy.test.js +10 -8
  32. package/test/recover.test.js +30 -23
  33. package/test/release-group.test.js +16 -17
  34. package/test/release-retention.test.js +10 -8
  35. package/test/release-runtime-retention.test.js +31 -39
  36. package/test/rollbridge.test.js +388 -395
  37. package/test/shutdown-completion.test.js +51 -51
  38. package/test/state-store.test.js +10 -8
  39. package/test/system-ids.test.js +15 -13
@@ -1,10 +1,11 @@
1
1
  // @ts-check
2
2
 
3
- import assert from "node:assert/strict"
4
3
  import http from "node:http"
5
- import test from "node:test"
4
+ import {describe, expect, test} from "@velocious/testing"
6
5
  import {waitForHealth} from "../src/health.js"
7
6
 
7
+ describe("health", () => {
8
+
8
9
  /**
9
10
  * Starts a health server that records when it first receives a probe.
10
11
  * @returns {Promise<{firstProbeDelay: () => number, port: number, close: () => Promise<void>}>} Server handle.
@@ -40,7 +41,7 @@ test("waitForHealth delays the first probe by startDelayMs", async () => {
40
41
  port: server.port
41
42
  })
42
43
 
43
- assert.ok(server.firstProbeDelay() >= 180, `expected first probe to be delayed ~200ms, was ${server.firstProbeDelay()}ms`)
44
+ expect({value: Boolean(server.firstProbeDelay() >= 180), context: `expected first probe to be delayed ~200ms, was ${server.firstProbeDelay()}ms`}).toMatchObject({value: true})
44
45
  } finally {
45
46
  await server.close()
46
47
  }
@@ -56,8 +57,9 @@ test("waitForHealth probes immediately when startDelayMs is 0", async () => {
56
57
  port: server.port
57
58
  })
58
59
 
59
- assert.ok(server.firstProbeDelay() < 150, `expected an immediate first probe, was ${server.firstProbeDelay()}ms`)
60
+ expect({value: Boolean(server.firstProbeDelay() < 150), context: `expected an immediate first probe, was ${server.firstProbeDelay()}ms`}).toMatchObject({value: true})
60
61
  } finally {
61
62
  await server.close()
62
63
  }
63
64
  })
65
+ })
package/test/logs.test.js CHANGED
@@ -1,15 +1,16 @@
1
1
  // @ts-check
2
2
 
3
- import assert from "node:assert/strict"
4
3
  import fs from "node:fs/promises"
5
4
  import os from "node:os"
6
5
  import path from "node:path"
7
- import test from "node:test"
6
+ import {describe, expect, test} from "@velocious/testing"
8
7
  import {fileURLToPath} from "node:url"
9
8
  import RollbridgeDaemon from "../src/daemon.js"
10
9
  import {normalizeConfig} from "../src/config.js"
11
10
  import {formatLogSources, runCli} from "../src/cli.js"
12
11
 
12
+ describe("logs", () => {
13
+
13
14
  const currentDir = path.dirname(fileURLToPath(import.meta.url))
14
15
  const dummyAppPath = path.join(currentDir, "fixtures", "dummy-app.js")
15
16
 
@@ -19,10 +20,10 @@ test("formatLogSources renders a section per process with timestamped lines", ()
19
20
  {id: "beacon", logs: [], source: "service"}
20
21
  ], undefined)
21
22
 
22
- assert.match(output, /== web \[release v1 \(active\)\] ==/)
23
- assert.match(output, /2026-05-22T00:00:00\.000Z \[stdout\] listening/)
24
- assert.match(output, /== beacon \[service\] ==/)
25
- assert.match(output, /\(no recent output\)/)
23
+ expect(output).toMatch(/== web \[release v1 \(active\)\] ==/)
24
+ expect(output).toMatch(/2026-05-22T00:00:00\.000Z \[stdout\] listening/)
25
+ expect(output).toMatch(/== beacon \[service\] ==/)
26
+ expect(output).toMatch(/\(no recent output\)/)
26
27
  })
27
28
 
28
29
  test("formatLogSources filters to a single process id", () => {
@@ -33,16 +34,13 @@ test("formatLogSources filters to a single process id", () => {
33
34
 
34
35
  const output = formatLogSources(sources, "web")
35
36
 
36
- assert.match(output, /== web /)
37
- assert.doesNotMatch(output, /beacon/)
37
+ expect(output).toMatch(/== web /)
38
+ expect(output).not.toMatch(/beacon/)
38
39
  })
39
40
 
40
41
  test("formatLogSources reports when there are no processes or no match", () => {
41
- assert.equal(formatLogSources([], undefined), "No managed processes.")
42
- assert.equal(
43
- formatLogSources([{id: "web", logs: [], source: "release v1 (active)"}], "missing"),
44
- 'No process found with id "missing".'
45
- )
42
+ expect(formatLogSources([], undefined)).toBe("No managed processes.")
43
+ expect(formatLogSources([{id: "web", logs: [], source: "release v1 (active)"}], "missing")).toBe('No process found with id "missing".')
46
44
  })
47
45
 
48
46
  test("logs CLI prints captured output per managed process", async () => {
@@ -80,7 +78,7 @@ test("logs CLI prints captured output per managed process", async () => {
80
78
  await daemon.deploy({releaseId: "v1", releasePath: root, revision: "v1"})
81
79
  await runCli(["node", "rollbridge", "logs", "-c", path.join(root, "rollbridge.js")])
82
80
 
83
- assert.match(lines.join("\n"), /== web \[release v1 \(active\)\] ==/)
81
+ expect(lines.join("\n")).toMatch(/== web \[release v1 \(active\)\] ==/)
84
82
 
85
83
  lines.length = 0
86
84
  await runCli(["node", "rollbridge", "logs", "--json", "-c", path.join(root, "rollbridge.js")])
@@ -88,12 +86,20 @@ test("logs CLI prints captured output per managed process", async () => {
88
86
  const parsed = JSON.parse(lines.join("\n"))
89
87
  const web = parsed.find((/** @type {{id: string, logs: import("../src/managed-process.js").ManagedProcessLog[], source: string}} */ entry) => entry.id === "web")
90
88
 
91
- assert.ok(web, "expected a web entry in the JSON output")
92
- assert.match(web.source, /release v1 \(active\)/)
93
- assert.ok(Array.isArray(web.logs))
89
+ if (!web) throw new Error("expected a web entry in the JSON output")
90
+ expect(web.source).toMatch(/release v1 \(active\)/)
91
+ expect(Array.isArray(web.logs)).toBeTruthy()
92
+
93
+ lines.length = 0
94
+ await runCli(["node", "rollbridge", "status", "--no-logs", "-c", path.join(root, "rollbridge.js")])
95
+
96
+ const status = JSON.parse(lines.join("\n"))
97
+
98
+ expect("logs" in status.releases[0].processes[0]).toBe(false)
94
99
  } finally {
95
100
  console.log = originalLog
96
101
  await daemon.shutdown()
97
102
  await fs.rm(root, {force: true, recursive: true})
98
103
  }
99
104
  })
105
+ })
@@ -1,13 +1,14 @@
1
1
  // @ts-check
2
2
 
3
- import assert from "node:assert/strict"
4
3
  import fs from "node:fs"
5
4
  import os from "node:os"
6
5
  import path from "node:path"
7
- import test from "node:test"
6
+ import {describe, expect, test} from "@velocious/testing"
8
7
  import {fileURLToPath} from "node:url"
9
8
  import ManagedProcess from "../src/managed-process.js"
10
9
 
10
+ describe("managed-process", () => {
11
+
11
12
  const crasherPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures", "crasher.js")
12
13
 
13
14
  /**
@@ -51,9 +52,9 @@ test("retains and reports only the configured number of recent output lines", ()
51
52
 
52
53
  const {logs} = managed.status()
53
54
 
54
- assert.equal(logs.length, 3)
55
- assert.deepEqual(logs.map((entry) => entry.line), ["c", "d", "e"])
56
- assert.equal(logs[0].stream, "stdout")
55
+ expect(logs.length).toBe(3)
56
+ expect(logs.map((entry) => entry.line)).toEqual(["c", "d", "e"])
57
+ expect(logs[0].stream).toBe("stdout")
57
58
  })
58
59
 
59
60
  test("keeps every output line when fewer than the retention limit are produced", () => {
@@ -63,17 +64,17 @@ test("keeps every output line when fewer than the retention limit are produced",
63
64
 
64
65
  const {logs} = managed.status()
65
66
 
66
- assert.deepEqual(logs.map((entry) => entry.line), ["one", "two"])
67
+ expect(logs.map((entry) => entry.line)).toEqual(["one", "two"])
67
68
  })
68
69
 
69
70
  test("reassembles output lines split across stream chunks", () => {
70
71
  const managed = buildProcess(50)
71
72
 
72
73
  managed.appendLog("stdout", "8191:x")
73
- assert.deepEqual(managed.status().logs, [])
74
+ expect(managed.status().logs).toEqual([])
74
75
 
75
76
  managed.appendLog("stdout", "xx\n")
76
- assert.deepEqual(managed.status().logs.map((entry) => entry.line), ["8191:xxx"])
77
+ expect(managed.status().logs.map((entry) => entry.line)).toEqual(["8191:xxx"])
77
78
  })
78
79
 
79
80
  test("bounds an output fragment that never terminates", () => {
@@ -82,8 +83,8 @@ test("bounds an output fragment that never terminates", () => {
82
83
 
83
84
  managed.appendLog("stdout", fragment)
84
85
 
85
- assert.equal(managed.status().logs.length, 15)
86
- assert.equal(managed.outputBuffers.stdout.length, 64 * 1024)
86
+ expect(managed.status().logs.length).toBe(15)
87
+ expect(managed.outputBuffers.stdout.length).toBe(64 * 1024)
87
88
  })
88
89
 
89
90
  test("emits each output line after retaining it", () => {
@@ -95,7 +96,7 @@ test("emits each output line after retaining it", () => {
95
96
  })
96
97
  managed.appendLog("stdout", "ready\n")
97
98
 
98
- assert.deepEqual(observed, {
99
+ expect(observed).toEqual({
99
100
  entry: managed.status().logs[0],
100
101
  retained: managed.status().logs
101
102
  })
@@ -104,10 +105,10 @@ test("emits each output line after retaining it", () => {
104
105
  test("reports zeroed restart and uptime fields before the process starts", () => {
105
106
  const status = buildProcess(50).status()
106
107
 
107
- assert.equal(status.restarts, 0)
108
- assert.equal(status.startedAt, undefined)
109
- assert.equal(status.uptimeMs, undefined)
110
- assert.equal(status.state, "stopped")
108
+ expect(status.restarts).toBe(0)
109
+ expect(status.startedAt).toBe(undefined)
110
+ expect(status.uptimeMs).toBe(undefined)
111
+ expect(status.state).toBe("stopped")
111
112
  })
112
113
 
113
114
  test("counts automatic restarts and reports startedAt and uptime while running", async () => {
@@ -128,15 +129,15 @@ test("counts automatic restarts and reports startedAt and uptime while running",
128
129
 
129
130
  const initial = managed.status()
130
131
 
131
- assert.equal(initial.restarts, 0)
132
- assert.equal(initial.state, "running")
133
- assert.equal(typeof initial.startedAt, "string")
134
- assert.ok(typeof initial.uptimeMs === "number" && initial.uptimeMs >= 0)
132
+ expect(initial.restarts).toBe(0)
133
+ expect(initial.state).toBe("running")
134
+ expect(typeof initial.startedAt).toBe("string")
135
+ expect(typeof initial.uptimeMs === "number" && initial.uptimeMs >= 0).toBeTruthy()
135
136
 
136
137
  // The fixture exits non-zero ~40ms after each start, so it keeps auto-restarting.
137
138
  await waitFor(() => managed.status().restarts >= 2)
138
139
 
139
- assert.ok(managed.status().restarts >= 2)
140
+ expect(managed.status().restarts >= 2).toBeTruthy()
140
141
  } finally {
141
142
  await managed.stop()
142
143
  }
@@ -162,7 +163,7 @@ test("a queued auto-restart timer is unref'd so it can't keep the process alive"
162
163
  // ref'd timer would respawn forever and block process exit, so the queued timer must be unref'd.
163
164
  await waitFor(() => managed.restartTimer !== undefined)
164
165
 
165
- assert.equal(managed.restartTimer?.hasRef(), false)
166
+ expect(managed.restartTimer?.hasRef()).toBe(false)
166
167
  } finally {
167
168
  await managed.stop()
168
169
  }
@@ -194,12 +195,12 @@ test("records the start reason, marking crash auto-restarts", async () => {
194
195
  try {
195
196
  await managed.start()
196
197
 
197
- assert.equal(managed.status().lastStartReason, "deploy")
198
+ expect(managed.status().lastStartReason).toBe("deploy")
198
199
 
199
200
  // The fixture crashes ~40ms after each start, so it auto-restarts with reason "crash".
200
201
  await waitFor(() => managed.status().restarts >= 1)
201
202
 
202
- assert.equal(managed.status().lastStartReason, "crash")
203
+ expect(managed.status().lastStartReason).toBe("crash")
203
204
  } finally {
204
205
  await managed.stop()
205
206
  }
@@ -212,7 +213,7 @@ test("records the manual start reason", async () => {
212
213
  try {
213
214
  await managed.start("manual")
214
215
 
215
- assert.equal(managed.status().lastStartReason, "manual")
216
+ expect(managed.status().lastStartReason).toBe("manual")
216
217
  } finally {
217
218
  await managed.stop()
218
219
  }
@@ -232,8 +233,8 @@ test("a later stop cancels a start queued behind an in-flight stop", async () =>
232
233
  finishStop()
233
234
  await Promise.all([queuedStart, finalStop])
234
235
  try {
235
- assert.equal(managed.status().pid, undefined)
236
- assert.equal(managed.status().state, "stopped")
236
+ expect(managed.status().pid).toBe(undefined)
237
+ expect(managed.status().state).toBe("stopped")
237
238
  } finally {
238
239
  await managed.stop()
239
240
  }
@@ -253,8 +254,8 @@ test("does not record a start reason when the spawn fails", async () => {
253
254
  })
254
255
 
255
256
  // The cwd does not exist, so the spawn fails before the process ever runs.
256
- await assert.rejects(() => managed.start("manual"))
257
- assert.equal(managed.status().lastStartReason, undefined)
257
+ await expect(managed.start("manual")).rejects.toThrow()
258
+ expect(managed.status().lastStartReason).toBe(undefined)
258
259
  })
259
260
 
260
261
  /**
@@ -298,9 +299,9 @@ test("runs quiet and drain lifecycle hooks before stopping", async () => {
298
299
  await managed.start()
299
300
  await managed.stop()
300
301
 
301
- assert.equal(managed.status().state, "stopped")
302
+ expect(managed.status().state).toBe("stopped")
302
303
  // quietCommand ran, then drainCommand, then the worker was stopped via stopSignal.
303
- assert.deepEqual(fs.readFileSync(logPath, "utf8").trim().split("\n"), ["quiet", "drain"])
304
+ expect(fs.readFileSync(logPath, "utf8").trim().split("\n")).toEqual(["quiet", "drain"])
304
305
  } finally {
305
306
  await managed.stop()
306
307
  fs.rmSync(dir, {force: true, recursive: true})
@@ -338,10 +339,10 @@ test("a configured stopCommand is used instead of the stop signal", async () =>
338
339
  await managed.start()
339
340
  await managed.stop()
340
341
 
341
- assert.equal(managed.status().state, "stopped")
342
- assert.deepEqual(fs.readFileSync(logPath, "utf8").trim().split("\n"), ["stop"])
342
+ expect(managed.status().state).toBe("stopped")
343
+ expect(fs.readFileSync(logPath, "utf8").trim().split("\n")).toEqual(["stop"])
343
344
  // The stop signal is replaced by the stop command (only a SIGKILL fallback may be sent).
344
- assert.ok(!signals.includes("SIGTERM"), `expected no stopSignal, got ${signals.join(",")}`)
345
+ expect({value: Boolean(!signals.includes("SIGTERM")), context: `expected no stopSignal, got ${signals.join(",")}`}).toMatchObject({value: true})
345
346
  } finally {
346
347
  await managed.stop()
347
348
  fs.rmSync(dir, {force: true, recursive: true})
@@ -376,7 +377,7 @@ test("stopCommand receives the retained process group id after the shell exits",
376
377
 
377
378
  await managed.stop()
378
379
 
379
- assert.equal(fs.readFileSync(pidPath, "utf8").trim(), String(pgid))
380
+ expect(fs.readFileSync(pidPath, "utf8").trim()).toBe(String(pgid))
380
381
  } finally {
381
382
  await managed.stop()
382
383
  fs.rmSync(dir, {force: true, recursive: true})
@@ -404,8 +405,8 @@ test("a failing lifecycle hook is logged but does not fail the stop", async () =
404
405
  await managed.start()
405
406
  await managed.stop()
406
407
 
407
- assert.equal(managed.status().state, "stopped")
408
- assert.ok(messages.includes("quiet command exited non-zero"), `expected a non-zero hook log, got ${messages.join(",")}`)
408
+ expect(managed.status().state).toBe("stopped")
409
+ expect({value: Boolean(messages.includes("quiet command exited non-zero")), context: `expected a non-zero hook log, got ${messages.join(",")}`}).toMatchObject({value: true})
409
410
  } finally {
410
411
  await managed.stop()
411
412
  }
@@ -433,9 +434,9 @@ test("a hanging lifecycle hook is bounded so stop still completes", async () =>
433
434
 
434
435
  await managed.stop()
435
436
 
436
- assert.equal(managed.status().state, "stopped")
437
+ expect(managed.status().state).toBe("stopped")
437
438
  // The hung quietCommand is killed at stopTimeoutMs rather than blocking stop indefinitely.
438
- assert.ok(Date.now() - startedAt < 5000, "stop should not wait for the hung hook")
439
+ expect(Date.now() - startedAt < 5000).toBe(true)
439
440
  } finally {
440
441
  await managed.stop()
441
442
  }
@@ -449,7 +450,7 @@ test("activateStrict runs the configured activation command once per call and re
449
450
  await managed.start()
450
451
  const pid = managed.pid
451
452
 
452
- assert.ok(pid)
453
+ expect(pid).toBeTruthy()
453
454
  managed.lifecycle = {activateCommand: "jobs activate", activateTimeoutMs: 60000, drainTimeoutMs: 0}
454
455
  managed.runHook = async (command, timeoutMs, label, hookPid) => {
455
456
  commands.push({command, label, pid: hookPid, timeoutMs})
@@ -457,10 +458,10 @@ test("activateStrict runs the configured activation command once per call and re
457
458
  }
458
459
 
459
460
  await managed.activateStrict()
460
- assert.deepEqual(commands, [{command: "jobs activate", label: "activate command", pid, timeoutMs: 60000}])
461
+ expect(commands).toEqual([{command: "jobs activate", label: "activate command", pid, timeoutMs: 60000}])
461
462
 
462
463
  managed.runHook = async () => new Error("activation rejected")
463
- await assert.rejects(() => managed.activateStrict(), /activation rejected/)
464
+ await expect(managed.activateStrict()).rejects.toThrow(/activation rejected/)
464
465
  } finally {
465
466
  await managed.stop()
466
467
  }
@@ -482,8 +483,8 @@ test("activateStrict rejects when the activated process is replaced while its ho
482
483
  return undefined
483
484
  }
484
485
 
485
- await assert.rejects(() => managed.activateStrict(), /exited before activation completed/)
486
- assert.equal(managed.lifecycleRole, "candidate")
486
+ await expect(managed.activateStrict()).rejects.toThrow(/exited before activation completed/)
487
+ expect(managed.lifecycleRole).toBe("candidate")
487
488
  } finally {
488
489
  managed.child = child
489
490
  managed.pid = pid
@@ -501,8 +502,8 @@ test("activateStrict rejects an activation request when its process is not runni
501
502
  return undefined
502
503
  }
503
504
 
504
- await assert.rejects(() => managed.activateStrict(), /is not running for activation/)
505
- assert.equal(hookRan, false)
505
+ await expect(managed.activateStrict()).rejects.toThrow(/is not running for activation/)
506
+ expect(hookRan).toBe(false)
506
507
  })
507
508
 
508
509
  test("reactivateStrict restores a retained quiesced process only after activation succeeds", async () => {
@@ -519,14 +520,14 @@ test("reactivateStrict restores a retained quiesced process only after activatio
519
520
  try {
520
521
  await managed.start()
521
522
  await managed.quiesceStrict()
522
- await assert.rejects(() => managed.reactivateStrict(), /restoration rejected/)
523
- assert.equal(managed.status().state, "quiesced")
524
- assert.equal(managed.status().lifecycleRole, "retired")
523
+ await expect(managed.reactivateStrict()).rejects.toThrow(/restoration rejected/)
524
+ expect(managed.status().state).toBe("quiesced")
525
+ expect(managed.status().lifecycleRole).toBe("retired")
525
526
 
526
527
  await managed.reactivateStrict()
527
- assert.equal(managed.status().state, "running")
528
- assert.equal(managed.status().lifecycleRole, "active")
529
- assert.deepEqual(hooks, ["quiet command", "activate command", "activate command"])
528
+ expect(managed.status().state).toBe("running")
529
+ expect(managed.status().lifecycleRole).toBe("active")
530
+ expect(hooks).toEqual(["quiet command", "activate command", "activate command"])
530
531
  } finally {
531
532
  await managed.stop()
532
533
  }
@@ -543,15 +544,15 @@ test("reactivateStrict retries a failed active-role startup against the retained
543
544
  }
544
545
 
545
546
  try {
546
- await assert.rejects(() => managed.start("deploy", "active"), /startup activation raced readiness/)
547
+ await expect(managed.start("deploy", "active")).rejects.toThrow(/startup activation raced readiness/)
547
548
  const failed = managed.status()
548
549
 
549
- assert.equal(failed.state, "failed")
550
- assert.ok(failed.pid)
550
+ expect(failed.state).toBe("failed")
551
+ expect(failed.pid).toBeTruthy()
551
552
  await managed.reactivateStrict()
552
- assert.equal(managed.status().state, "running")
553
- assert.equal(managed.status().lifecycleRole, "active")
554
- assert.equal(managed.status().pid, failed.pid)
553
+ expect(managed.status().state).toBe("running")
554
+ expect(managed.status().lifecycleRole).toBe("active")
555
+ expect(managed.status().pid).toBe(failed.pid)
555
556
  } finally {
556
557
  await managed.stop()
557
558
  }
@@ -579,19 +580,22 @@ test("quiesce waits for active-role restoration before retiring a restarted proc
579
580
  }
580
581
  return undefined
581
582
  }
582
- const start = assert.rejects(() => managed.start("crash", "active"), /quiesced before lifecycle role active was restored/)
583
+ const start = (async () => {
584
+ await expect(managed.start("crash", "active")).rejects.toThrow(/quiesced before lifecycle role active was restored/)
585
+ })()
583
586
 
584
587
  try {
585
588
  await activationStarted
586
589
  const quiesce = managed.quiesceStrict()
587
590
 
588
591
  await Promise.resolve()
589
- assert.deepEqual(hooks, ["activate:start"], "retirement must not race ahead of role restoration")
592
+ // Retirement must not race ahead of role restoration.
593
+ expect(hooks).toEqual(["activate:start"])
590
594
  allowActivation()
591
595
  await Promise.all([start, quiesce])
592
- assert.deepEqual(hooks, ["activate:start", "activate:end", "quiet"])
593
- assert.equal(managed.status().state, "quiesced")
594
- assert.equal(managed.lifecycleRole, "retired")
596
+ expect(hooks).toEqual(["activate:start", "activate:end", "quiet"])
597
+ expect(managed.status().state).toBe("quiesced")
598
+ expect(managed.lifecycleRole).toBe("retired")
595
599
  } finally {
596
600
  allowActivation()
597
601
  await start.catch(() => {})
@@ -630,8 +634,8 @@ test("sends the configured stopSignal as the graceful stop signal", async () =>
630
634
  await managed.stop()
631
635
 
632
636
  // The graceful stop reaches the ready descendant and its shell leader without SIGKILL.
633
- assert.deepEqual(signals, ["SIGINT", "SIGINT"])
634
- assert.equal(managed.status().state, "stopped")
637
+ expect(signals).toEqual(["SIGINT", "SIGINT"])
638
+ expect(managed.status().state).toBe("stopped")
635
639
  } finally {
636
640
  await managed.stop()
637
641
  fs.rmSync(dir, {force: true, recursive: true})
@@ -664,8 +668,8 @@ test("indefinite stop waits for the process to exit without SIGKILL", async () =
664
668
  await managed.start()
665
669
  await managed.stop()
666
670
 
667
- assert.equal(managed.status().state, "stopped")
668
- assert.deepEqual(signals, ["SIGTERM", "SIGTERM"])
671
+ expect(managed.status().state).toBe("stopped")
672
+ expect(signals).toEqual(["SIGTERM", "SIGTERM"])
669
673
  } finally {
670
674
  await managed.stop()
671
675
  }
@@ -704,9 +708,9 @@ test("stop waits for process group descendants after the detached shell exits",
704
708
 
705
709
  const elapsedMs = Date.now() - startedAt
706
710
 
707
- assert.ok(elapsedMs >= 250, `stop resolved after only ${elapsedMs}ms`)
708
- assert.ok(elapsedMs < 1500, `stop took ${elapsedMs}ms`)
709
- assert.equal(fs.readFileSync(latePath, "utf8"), "late")
711
+ expect({value: Boolean(elapsedMs >= 250), context: `stop resolved after only ${elapsedMs}ms`}).toMatchObject({value: true})
712
+ expect({value: Boolean(elapsedMs < 1500), context: `stop took ${elapsedMs}ms`}).toMatchObject({value: true})
713
+ expect(fs.readFileSync(latePath, "utf8")).toBe("late")
710
714
  } finally {
711
715
  await managed.stop()
712
716
  fs.rmSync(dir, {force: true, recursive: true})
@@ -742,7 +746,7 @@ test("stop does not return while a gracefully stopped descendant remains unreape
742
746
 
743
747
  await managed.stop()
744
748
 
745
- assert.throws(() => process.kill(childPid, 0), {code: "ESRCH"})
749
+ await expect(Promise.resolve().then(() => process.kill(childPid, 0))).rejects.toMatchObject({code: "ESRCH"})
746
750
  } finally {
747
751
  await managed.stop()
748
752
  fs.rmSync(dir, {force: true, recursive: true})
@@ -773,7 +777,7 @@ test("descendant reaping and leader shutdown share one graceful deadline", async
773
777
  try {
774
778
  await managed.stop({timeoutMs: 150})
775
779
 
776
- assert.deepEqual(calls.slice(0, 2), [
780
+ expect(calls.slice(0, 2)).toEqual([
777
781
  {deadline: 1150, signal: "SIGTERM"},
778
782
  {deadline: 1150}
779
783
  ])
@@ -789,9 +793,9 @@ test("a memory restart respawns and is counted when the supervisor still wants t
789
793
  await managed.start()
790
794
  await managed.restartForMemory()
791
795
 
792
- assert.equal(managed.status().state, "running")
793
- assert.equal(managed.memoryRestarts, 1)
794
- assert.equal(managed.status().lastStartReason, "memory")
796
+ expect(managed.status().state).toBe("running")
797
+ expect(managed.memoryRestarts).toBe(1)
798
+ expect(managed.status().lastStartReason).toBe("memory")
795
799
  } finally {
796
800
  await managed.stop()
797
801
  }
@@ -803,14 +807,14 @@ test("a memory restart does not respawn when shouldRestart is false", async () =
803
807
 
804
808
  try {
805
809
  await managed.start()
806
- assert.equal(managed.status().state, "running")
810
+ expect(managed.status().state).toBe("running")
807
811
 
808
812
  // The supervisor (e.g. daemon shutdown or a draining release) no longer wants it running.
809
813
  allowRestart = false
810
814
  await managed.restartForMemory()
811
815
 
812
- assert.equal(managed.status().state, "stopped")
813
- assert.equal(managed.memoryRestarts, 0)
816
+ expect(managed.status().state).toBe("stopped")
817
+ expect(managed.memoryRestarts).toBe(0)
814
818
  } finally {
815
819
  await managed.stop()
816
820
  }
@@ -826,8 +830,8 @@ test("does not auto-restart when the restart policy is disabled (maxRestarts: 0)
826
830
  await waitFor(() => managed.status().state === "failed")
827
831
  await new Promise((resolve) => setTimeout(resolve, 100))
828
832
 
829
- assert.equal(managed.status().restarts, 0)
830
- assert.equal(managed.status().state, "failed")
833
+ expect(managed.status().restarts).toBe(0)
834
+ expect(managed.status().state).toBe("failed")
831
835
  } finally {
832
836
  await managed.stop()
833
837
  }
@@ -847,9 +851,9 @@ test("stops auto-restarting once maxRestarts within the window is reached", asyn
847
851
  await waitFor(() => managed.status().restarts === 2 && managed.status().state === "failed")
848
852
  await new Promise((resolve) => setTimeout(resolve, 100))
849
853
 
850
- assert.equal(managed.status().restarts, 2)
851
- assert.equal(managed.status().state, "failed")
852
- assert.deepEqual(events.find((event) => event.message === "restart limit reached")?.data, {
854
+ expect(managed.status().restarts).toBe(2)
855
+ expect(managed.status().state).toBe("failed")
856
+ expect(events.find((event) => event.message === "restart limit reached")?.data).toEqual({
853
857
  id: "crasher",
854
858
  maxRestarts: 2,
855
859
  windowMs: 60000
@@ -863,24 +867,24 @@ test("applies exponential backoff to restart delays, capped by maxDelayMs", () =
863
867
  const capped = buildCrasher({backoffFactor: 2, maxDelayMs: 500, maxRestarts: undefined, windowMs: 0})
864
868
 
865
869
  // restartDelayMs (10) * 2 ** attempt, capped at 500.
866
- assert.equal(capped.restartDelayFor(0), 10)
867
- assert.equal(capped.restartDelayFor(1), 20)
868
- assert.equal(capped.restartDelayFor(2), 40)
869
- assert.equal(capped.restartDelayFor(6), 500) // 10 * 64 = 640, capped to 500
870
- assert.equal(capped.restartDelayFor(7), 500)
870
+ expect(capped.restartDelayFor(0)).toBe(10)
871
+ expect(capped.restartDelayFor(1)).toBe(20)
872
+ expect(capped.restartDelayFor(2)).toBe(40)
873
+ expect(capped.restartDelayFor(6)).toBe(500) // 10 * 64 = 640, capped to 500
874
+ expect(capped.restartDelayFor(7)).toBe(500)
871
875
 
872
876
  // maxDelayMs: 0 means no cap.
873
877
  const uncapped = buildCrasher({backoffFactor: 3, maxDelayMs: 0, maxRestarts: undefined, windowMs: 0})
874
878
 
875
- assert.equal(uncapped.restartDelayFor(0), 10)
876
- assert.equal(uncapped.restartDelayFor(2), 90)
879
+ expect(uncapped.restartDelayFor(0)).toBe(10)
880
+ expect(uncapped.restartDelayFor(2)).toBe(90)
877
881
  })
878
882
 
879
883
  test("the unlimited constant-delay fast path still applies maxDelayMs", () => {
880
884
  // restartDelayMs (10) above maxDelayMs (5), with no backoff and unlimited restarts.
881
885
  const managed = buildCrasher({backoffFactor: 1, maxDelayMs: 5, maxRestarts: undefined, windowMs: 0})
882
886
 
883
- assert.equal(managed.restartDelayFor(0), 5)
887
+ expect(managed.restartDelayFor(0)).toBe(5)
884
888
 
885
889
  /** @type {number | undefined} */
886
890
  let queued
@@ -888,5 +892,6 @@ test("the unlimited constant-delay fast path still applies maxDelayMs", () => {
888
892
  managed.queueRestart = (delayMs) => { queued = delayMs }
889
893
  managed.scheduleRestart()
890
894
 
891
- assert.equal(queued, 5)
895
+ expect(queued).toBe(5)
896
+ })
892
897
  })