rollbridge 0.1.54 → 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.
@@ -1,13 +1,12 @@
1
1
  // @ts-check
2
2
 
3
- import assert from "node:assert/strict"
4
3
  import {spawn} from "node:child_process"
5
4
  import {once} from "node:events"
6
5
  import fs from "node:fs/promises"
7
6
  import net from "node:net"
8
7
  import os from "node:os"
9
8
  import path from "node:path"
10
- import test from "node:test"
9
+ import {describe, expect, test} from "@velocious/testing"
11
10
  import {fileURLToPath, pathToFileURL} from "node:url"
12
11
  import RollbridgeDaemon from "../src/daemon.js"
13
12
  import {normalizeConfig} from "../src/config.js"
@@ -15,6 +14,8 @@ import {sendControlCommand} from "../src/control-client.js"
15
14
  import {liveProcesses, readState, writeState} from "../src/state-store.js"
16
15
  import {runCli} from "../src/cli.js"
17
16
 
17
+ describe("rollbridge", () => {
18
+
18
19
  const currentDir = path.dirname(fileURLToPath(import.meta.url))
19
20
  const binPath = path.join(currentDir, "..", "bin", "rollbridge")
20
21
  const dependentAppPath = path.join(currentDir, "fixtures", "dependent-app.js")
@@ -22,6 +23,7 @@ const dummyAppPath = path.join(currentDir, "fixtures", "dummy-app.js")
22
23
  const memoryHogPath = path.join(currentDir, "fixtures", "memory-hog.js")
23
24
  const serviceAppPath = path.join(currentDir, "fixtures", "service-app.js")
24
25
  const singletonAppPath = path.join(currentDir, "fixtures", "singleton-app.js")
26
+ const linuxTest = process.platform === "linux" ? test : test.skip
25
27
 
26
28
  test("a nonBlockingDrain worker stops immediately while its release is still draining", async () => {
27
29
  const fixture = await createFixture({nonBlockingDrainWorker: true})
@@ -47,9 +49,9 @@ test("a nonBlockingDrain worker stops immediately while its release is still dra
47
49
 
48
50
  // The release is still draining (the WebSocket is held) and its proxied process is still
49
51
  // serving, but the worker has already drained.
50
- assert.equal(v1.state, "draining")
51
- assert.equal(v1.processes.find((processStatus) => processStatus.id === "web")?.state, "running")
52
- assert.equal(v1.processes.find((processStatus) => processStatus.id === "worker")?.state, "stopped")
52
+ expect(v1.state).toBe("draining")
53
+ expect(v1.processes.find((processStatus) => processStatus.id === "web")?.state).toBe("running")
54
+ expect(v1.processes.find((processStatus) => processStatus.id === "worker")?.state).toBe("stopped")
53
55
  } finally {
54
56
  if (socket) socket.close()
55
57
  await daemon.shutdown()
@@ -63,16 +65,16 @@ test("deploy switches new HTTP traffic while old WebSockets drain", async () =>
63
65
 
64
66
  try {
65
67
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
66
- assert.equal(await fetchText(daemon, "/release"), "v1")
68
+ expect(await fetchText(daemon, "/release")).toBe("v1")
67
69
 
68
70
  const websocket = await openWebSocket(daemon)
69
71
 
70
72
  await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
71
- assert.equal(await fetchText(daemon, "/release"), "v2")
73
+ expect(await fetchText(daemon, "/release")).toBe("v2")
72
74
 
73
75
  const drainingRelease = statusRelease(daemon, "v1")
74
- assert.equal(drainingRelease.state, "draining")
75
- assert.equal(drainingRelease.connections.websocket, 1)
76
+ expect(drainingRelease.state).toBe("draining")
77
+ expect(drainingRelease.connections.websocket).toBe(1)
76
78
 
77
79
  websocket.close()
78
80
  await waitFor(async () => statusRelease(daemon, "v1").state === "stopped")
@@ -89,13 +91,10 @@ test("failed health check leaves the previous release active", async () => {
89
91
  try {
90
92
  await daemon.deploy({releaseId: "good", releasePath: fixture.root, revision: "good"})
91
93
 
92
- await assert.rejects(
93
- () => daemon.deploy({releaseId: "bad", releasePath: fixture.root, revision: "bad"}),
94
- /Health check failed/
95
- )
94
+ await expect(daemon.deploy({releaseId: "bad", releasePath: fixture.root, revision: "bad"})).rejects.toThrow(/Health check failed/)
96
95
 
97
- assert.equal(await fetchText(daemon, "/release"), "good")
98
- assert.equal(daemon.status().activeReleaseId, "good")
96
+ expect(await fetchText(daemon, "/release")).toBe("good")
97
+ expect(daemon.status().activeReleaseId).toBe("good")
99
98
  } finally {
100
99
  await daemon.shutdown()
101
100
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -128,8 +127,8 @@ test("deploy reloads process config and retires the previous worker with the ref
128
127
  await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
129
128
  await waitFor(() => statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "worker")?.state === "stopped", 1000)
130
129
 
131
- assert.equal(daemon.config.processes.find((processConfig) => processConfig.id === "worker")?.gracefulStopMs, 50)
132
- assert.equal(await fetchText(daemon, "/release"), "v2")
130
+ expect(daemon.config.processes.find((processConfig) => processConfig.id === "worker")?.gracefulStopMs).toBe(50)
131
+ expect(await fetchText(daemon, "/release")).toBe("v2")
133
132
  } finally {
134
133
  await daemon.shutdown()
135
134
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -149,12 +148,9 @@ test("deploy rejects a reloaded config that changes the running proxy", async ()
149
148
  proxy: {...fixture.config.proxy, host: "0.0.0.0"}
150
149
  }), fixture.root)
151
150
 
152
- await assert.rejects(
153
- () => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
154
- /proxy\.host.*restart the Rollbridge daemon/
155
- )
156
- assert.equal(daemon.status().activeReleaseId, "v1")
157
- assert.equal(await fetchText(daemon, "/release"), "v1")
151
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/proxy\.host.*restart the Rollbridge daemon/)
152
+ expect(daemon.status().activeReleaseId).toBe("v1")
153
+ expect(await fetchText(daemon, "/release")).toBe("v1")
158
154
  } finally {
159
155
  await daemon.shutdown()
160
156
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -178,14 +174,11 @@ test("a failed deploy does not adopt reloaded process config", async () => {
178
174
  })
179
175
 
180
176
  await writeConfigFile(failingConfig, fixture.root)
181
- await assert.rejects(
182
- () => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
183
- /Health check failed/
184
- )
185
-
186
- assert.equal(daemon.config.processes.find((processConfig) => processConfig.id === "web")?.health?.path, "/ping")
187
- assert.equal(daemon.status().activeReleaseId, "v1")
188
- assert.equal(await fetchText(daemon, "/release"), "v1")
177
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/Health check failed/)
178
+
179
+ expect(daemon.config.processes.find((processConfig) => processConfig.id === "web")?.health?.path).toBe("/ping")
180
+ expect(daemon.status().activeReleaseId).toBe("v1")
181
+ expect(await fetchText(daemon, "/release")).toBe("v1")
189
182
  } finally {
190
183
  await daemon.shutdown()
191
184
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -202,11 +195,11 @@ test("wildcard proxy bind host targets release processes through loopback", asyn
202
195
  const status = daemon.status()
203
196
  const release = statusRelease(daemon, "v1")
204
197
 
205
- assert.ok(daemon.activeRelease, "expected active release")
206
- assert.equal(status.proxy.host, "0.0.0.0")
207
- assert.equal(status.proxy.upstreamHost, "127.0.0.1")
208
- assert.equal(daemon.activeRelease.proxyTarget().target, `http://127.0.0.1:${release.ports.web}`)
209
- assert.equal(await fetchText(daemon, "/release"), "v1")
198
+ if (!daemon.activeRelease) throw new Error("expected active release")
199
+ expect(status.proxy.host).toBe("0.0.0.0")
200
+ expect(status.proxy.upstreamHost).toBe("127.0.0.1")
201
+ expect(daemon.activeRelease.proxyTarget().target).toBe(`http://127.0.0.1:${release.ports.web}`)
202
+ expect(await fetchText(daemon, "/release")).toBe("v1")
210
203
  } finally {
211
204
  await daemon.shutdown()
212
205
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -225,27 +218,24 @@ test("failed release startup logs process output and cleanup status", async () =
225
218
  await daemon.start()
226
219
 
227
220
  try {
228
- await assert.rejects(
229
- () => daemon.deploy({releaseId: "bad", releasePath: fixture.root, revision: "bad"}),
230
- /Health check failed/
231
- )
221
+ await expect(daemon.deploy({releaseId: "bad", releasePath: fixture.root, revision: "bad"})).rejects.toThrow(/Health check failed/)
232
222
 
233
223
  const processStatusLog = logs.find((entry) => entry.message === "release startup process status" && entry.data?.phase === "before cleanup" && entry.data?.processId === "web")
234
224
  const cleanupProcessStatusLog = logs.find((entry) => entry.message === "release startup process status" && entry.data?.phase === "after cleanup" && entry.data?.processId === "web")
235
225
  const handoffServiceStatusLog = logs.find((entry) => entry.message === "release startup process status" && entry.data?.phase === "after cleanup" && entry.data?.processId === "beacon")
236
226
 
237
- assert.ok(processStatusLog, "expected failed web process diagnostics to be logged")
238
- assert.ok(processStatusLog.data, "expected diagnostic data")
239
- assert.ok(Array.isArray(processStatusLog.data.logs), "expected retained process output in diagnostics")
240
- assert.ok(processStatusLog.data.logs.some((entry) => typeof entry === "object" && entry && "line" in entry && entry.line === "startup stdout"))
241
- assert.ok(processStatusLog.data.logs.some((entry) => typeof entry === "object" && entry && "line" in entry && entry.line === "startup stderr"))
242
- assert.equal(processStatusLog.data.state, "running")
243
- assert.ok(cleanupProcessStatusLog, "expected failed web cleanup diagnostics to be logged")
244
- assert.equal(cleanupProcessStatusLog.data?.state, "stopped")
245
- assert.equal(cleanupProcessStatusLog.data?.exitSignal, "SIGTERM")
246
- assert.ok(handoffServiceStatusLog, "expected handoff service cleanup diagnostics to be logged")
247
- assert.equal(handoffServiceStatusLog.data?.state, "stopped")
248
- assert.equal(handoffServiceStatusLog.data?.exitSignal, "SIGTERM")
227
+ if (!processStatusLog) throw new Error("expected failed web process diagnostics to be logged")
228
+ if (!processStatusLog.data) throw new Error("expected diagnostic data")
229
+ if (!Array.isArray(processStatusLog.data.logs)) throw new Error("expected retained process output in diagnostics")
230
+ expect(processStatusLog.data.logs.some((entry) => typeof entry === "object" && entry && "line" in entry && entry.line === "startup stdout")).toBeTruthy()
231
+ expect(processStatusLog.data.logs.some((entry) => typeof entry === "object" && entry && "line" in entry && entry.line === "startup stderr")).toBeTruthy()
232
+ expect(processStatusLog.data.state).toBe("running")
233
+ if (!cleanupProcessStatusLog) throw new Error("expected failed web cleanup diagnostics to be logged")
234
+ expect(cleanupProcessStatusLog.data?.state).toBe("stopped")
235
+ expect(cleanupProcessStatusLog.data?.exitSignal).toBe("SIGTERM")
236
+ if (!handoffServiceStatusLog) throw new Error("expected handoff service cleanup diagnostics to be logged")
237
+ expect(handoffServiceStatusLog.data?.state).toBe("stopped")
238
+ expect(handoffServiceStatusLog.data?.exitSignal).toBe("SIGTERM")
249
239
  } finally {
250
240
  await daemon.shutdown()
251
241
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -270,8 +260,8 @@ test("singleton processes restart without overlap during deploy", async () => {
270
260
 
271
261
  const status = daemon.status()
272
262
 
273
- assert.equal(status.singletons.length, 1)
274
- assert.equal(status.singletons[0].process.state, "running")
263
+ expect(status.singletons.length).toBe(1)
264
+ expect(status.singletons[0].process.state).toBe("running")
275
265
  } finally {
276
266
  await daemon.shutdown()
277
267
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -319,9 +309,12 @@ test("candidate activation quiesces the old jobs generation before a blocked sin
319
309
  await singletonReplacementBlocked
320
310
  await Promise.resolve()
321
311
 
322
- assert.equal(daemon.status().activeReleaseId, "v2", "candidate traffic must already be active")
323
- assert.equal(deploySettled, false, "deploy must remain pending on singleton replacement")
324
- assert.equal(await fs.readFile(fixture.serviceQuietPath, "utf8"), "v1\n", "old jobs-main must quiesce before singleton replacement completes")
312
+ // Candidate traffic must already be active.
313
+ expect(daemon.status().activeReleaseId).toBe("v2")
314
+ // Deploy must remain pending on singleton replacement.
315
+ expect(deploySettled).toBe(false)
316
+ // Old jobs-main must quiesce before singleton replacement completes.
317
+ expect(await fs.readFile(fixture.serviceQuietPath, "utf8")).toBe("v1\n")
325
318
 
326
319
  await fs.writeFile(singletonGatePath, "continue\n")
327
320
  singletonGateReleased = true
@@ -350,7 +343,7 @@ test("a failed singleton replacement surfaces the error after stopping the old s
350
343
  await waitFor(async () => (await processEvents(fixture.singletonLogPath)).some((event) => event.event === "start" && event.releaseId === "v1"))
351
344
 
352
345
  // The new release's singleton fails to start, so the deploy surfaces the error.
353
- await assert.rejects(() => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}))
346
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow()
354
347
 
355
348
  // The old singleton is stopped before the new one is started, so two copies never
356
349
  // overlap — even when the replacement then fails.
@@ -360,9 +353,9 @@ test("a failed singleton replacement surfaces the error after stopping the old s
360
353
 
361
354
  // Traffic switches before singletons are replaced, so the new release is already active,
362
355
  // but its singleton is left failed with no replacement running.
363
- assert.equal(status.activeReleaseId, "v2")
364
- assert.equal(status.singletons.length, 1)
365
- assert.equal(status.singletons[0].process.state, "failed")
356
+ expect(status.activeReleaseId).toBe("v2")
357
+ expect(status.singletons.length).toBe(1)
358
+ expect(status.singletons[0].process.state).toBe("failed")
366
359
  } finally {
367
360
  await daemon.shutdown()
368
361
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -379,15 +372,15 @@ test("service processes start before releases and restart with the latest deploy
379
372
 
380
373
  const firstServiceStatus = daemon.status().services[0].process
381
374
 
382
- assert.ok(firstServiceStatus.pid, "service should have a pid")
383
- assert.match(firstServiceStatus.command, /v1/)
375
+ expect(firstServiceStatus.pid).toBeTruthy()
376
+ expect(firstServiceStatus.command).toMatch(/v1/)
384
377
 
385
378
  await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
386
379
 
387
380
  const secondServiceStatus = daemon.status().services[0].process
388
381
 
389
- assert.equal(secondServiceStatus.pid, firstServiceStatus.pid)
390
- assert.match(secondServiceStatus.command, /v2/)
382
+ expect(secondServiceStatus.pid).toBe(firstServiceStatus.pid)
383
+ expect(secondServiceStatus.command).toMatch(/v2/)
391
384
 
392
385
  process.kill(-Number(secondServiceStatus.pid), "SIGTERM")
393
386
  await waitFor(async () => {
@@ -413,28 +406,28 @@ test("handoff services start per release and drain with their release", async ()
413
406
  const v1 = statusRelease(daemon, "v1")
414
407
  const v1Service = v1.processes.find((processStatus) => processStatus.id === "beacon")
415
408
 
416
- assert.ok(v1Service?.pid, "v1 service should be running")
417
- assert.equal(v1.ports.beacon > 0, true)
409
+ expect(v1Service?.pid).toBeTruthy()
410
+ expect(v1.ports.beacon > 0).toBe(true)
418
411
 
419
412
  await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
420
413
  const v2 = statusRelease(daemon, "v2")
421
414
  const v2Service = v2.processes.find((processStatus) => processStatus.id === "beacon")
422
415
 
423
- assert.ok(v2Service?.pid, "v2 service should be running")
424
- assert.notEqual(v2.ports.beacon, v1.ports.beacon)
425
- assert.equal(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "beacon")?.state, "quiesced")
416
+ expect(v2Service?.pid).toBeTruthy()
417
+ expect(v2.ports.beacon).not.toBe(v1.ports.beacon)
418
+ expect(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "beacon")?.state).toBe("quiesced")
426
419
 
427
420
  socket.close()
428
421
  socket = undefined
429
422
 
430
423
  await waitFor(() => statusRelease(daemon, "v1").state === "stopped")
431
- assert.equal(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "beacon")?.state, "stopped")
424
+ expect(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "beacon")?.state).toBe("stopped")
432
425
 
433
426
  const events = await processEvents(fixture.serviceLogPath)
434
427
 
435
- assert.ok(events.some((event) => event.event === "start" && event.releaseId === "v1"), "v1 service should start")
436
- assert.ok(events.some((event) => event.event === "start" && event.releaseId === "v2"), "v2 service should start")
437
- assert.ok(events.some((event) => event.event === "stop" && event.releaseId === "v1"), "v1 service should stop after drain")
428
+ expect(events.some((event) => event.event === "start" && event.releaseId === "v1")).toBe(true)
429
+ expect(events.some((event) => event.event === "start" && event.releaseId === "v2")).toBe(true)
430
+ expect(events.some((event) => event.event === "stop" && event.releaseId === "v1")).toBe(true)
438
431
  } finally {
439
432
  if (socket) socket.close()
440
433
  await daemon.shutdown()
@@ -457,8 +450,8 @@ test("handoff services stop after release-local dependents finish draining", asy
457
450
 
458
451
  const drainingRelease = statusRelease(daemon, "v1")
459
452
 
460
- assert.equal(drainingRelease.state, "draining")
461
- assert.equal(drainingRelease.processes.find((processStatus) => processStatus.id === "beacon")?.state, "quiesced")
453
+ expect(drainingRelease.state).toBe("draining")
454
+ expect(drainingRelease.processes.find((processStatus) => processStatus.id === "beacon")?.state).toBe("quiesced")
462
455
 
463
456
  socket.close()
464
457
  socket = undefined
@@ -467,7 +460,8 @@ test("handoff services stop after release-local dependents finish draining", asy
467
460
  const events = await processEvents(fixture.serviceLogPath)
468
461
  const v1ServiceStop = events.find((event) => event.event === "stop" && event.releaseId === "v1")
469
462
 
470
- assert.ok(v1ServiceStop, "v1 handoff service should stop after release drain")
463
+ // V1 handoff service should stop after release drain.
464
+ expect(v1ServiceStop).toBeTruthy()
471
465
  } finally {
472
466
  if (socket) socket.close()
473
467
  await daemon.shutdown()
@@ -485,20 +479,26 @@ test("candidate activation retires jobs-main with its workers without waiting fo
485
479
  const oldService = oldRelease.processes.find((processStatus) => processStatus.id === "beacon")
486
480
  const oldWorker = oldRelease.processes.find((processStatus) => processStatus.id === "worker")
487
481
 
488
- assert.ok(oldService?.pid)
489
- assert.ok(oldWorker?.pid)
482
+ expect(oldService?.pid).toBeTruthy()
483
+ expect(oldWorker?.pid).toBeTruthy()
490
484
 
491
485
  await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
492
486
 
493
- assert.equal(daemon.status().activeReleaseId, "v2", "traffic must switch only after the complete candidate is healthy")
494
- assert.equal(await fs.readFile(fixture.serviceQuietPath, "utf8"), "v1\n", "old jobs-main must quiesce immediately after candidate activation")
487
+ // Traffic must switch only after the complete candidate is healthy.
488
+ expect(daemon.status().activeReleaseId).toBe("v2")
489
+ // Old jobs-main must quiesce immediately after candidate activation.
490
+ expect(await fs.readFile(fixture.serviceQuietPath, "utf8")).toBe("v1\n")
495
491
 
496
492
  const retired = statusRelease(daemon, "v1")
497
493
 
498
- assert.equal(retired.state, "draining", "deployment completion must not wait for the old jobs generation")
499
- assert.equal(retired.processes.find((processStatus) => processStatus.id === "beacon")?.state, "quiesced", "old jobs-main must remain alive and quiesced with its draining workers")
500
- assert.notEqual(retired.processes.find((processStatus) => processStatus.id === "worker")?.state, "stopped", "old worker must remain in its original generation until accepted work settles")
501
- assert.notEqual(statusRelease(daemon, "v2").ports.beacon, retired.ports.beacon, "old and new workers must retain distinct jobs-main endpoints")
494
+ // Deployment completion must not wait for the old jobs generation.
495
+ expect(retired.state).toBe("draining")
496
+ // Old jobs-main must remain alive and quiesced with its draining workers.
497
+ expect(retired.processes.find((processStatus) => processStatus.id === "beacon")?.state).toBe("quiesced")
498
+ // Old worker must remain in its original generation until accepted work settles.
499
+ expect(retired.processes.find((processStatus) => processStatus.id === "worker")?.state).not.toBe("stopped")
500
+ // Old and new workers must retain distinct jobs-main endpoints.
501
+ expect(statusRelease(daemon, "v2").ports.beacon).not.toBe(retired.ports.beacon)
502
502
  } finally {
503
503
  await daemon.shutdown()
504
504
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -511,14 +511,14 @@ test("opt-in generation lifecycle acknowledges old retirement before activating
511
511
 
512
512
  try {
513
513
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
514
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1"])
515
- assert.equal(daemon.status().activeReleaseId, "v1")
514
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1"])
515
+ expect(daemon.status().activeReleaseId).toBe("v1")
516
516
 
517
517
  await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
518
518
 
519
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "retire:v1", "activate:v2"])
520
- assert.equal(daemon.status().activeReleaseId, "v2")
521
- assert.equal(daemon.status().generationTransition?.phase, "committed")
519
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "retire:v1", "activate:v2"])
520
+ expect(daemon.status().activeReleaseId).toBe("v2")
521
+ expect(daemon.status().generationTransition?.phase).toBe("committed")
522
522
  } finally {
523
523
  await daemon.shutdown()
524
524
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -535,11 +535,11 @@ test("manual restart reaches the active handoff coordinator and restores its lif
535
535
  const result = await daemon.restartProcesses({processId: "beacon"})
536
536
  const after = statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "beacon")?.pid
537
537
 
538
- assert.deepEqual(result, {restarted: ["beacon"]})
539
- assert.ok(before)
540
- assert.ok(after)
541
- assert.notEqual(after, before)
542
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "retire:v1", "activate:v1"])
538
+ expect(result).toEqual({restarted: ["beacon"]})
539
+ expect(before).toBeTruthy()
540
+ expect(after).toBeTruthy()
541
+ expect(after).not.toBe(before)
542
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "retire:v1", "activate:v1"])
543
543
  } finally {
544
544
  await daemon.shutdown()
545
545
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -574,14 +574,14 @@ test("generation commit is durable before awaited post-transition work", async (
574
574
 
575
575
  const persisted = /** @type {{activeReleaseId?: string, generationTransition?: {phase?: string}, singletonReleaseIds?: Record<string, string>} | undefined} */ (await readState(fixture.statePath))
576
576
 
577
- assert.equal(daemon.status().activeReleaseId, "v2")
578
- assert.equal(persisted?.activeReleaseId, "v2")
579
- assert.equal(persisted?.generationTransition?.phase, "committed_pending")
580
- assert.equal(persisted?.singletonReleaseIds?.["jobs-main"], "v1")
577
+ expect(daemon.status().activeReleaseId).toBe("v2")
578
+ expect(persisted?.activeReleaseId).toBe("v2")
579
+ expect(persisted?.generationTransition?.phase).toBe("committed_pending")
580
+ expect(persisted?.singletonReleaseIds?.["jobs-main"]).toBe("v1")
581
581
 
582
582
  releaseReplacement()
583
583
  await deployPromise
584
- assert.equal(daemon.status().generationTransition?.phase, "committed")
584
+ expect(daemon.status().generationTransition?.phase).toBe("committed")
585
585
  } finally {
586
586
  releaseReplacement()
587
587
  await deployPromise?.catch(() => {})
@@ -598,18 +598,19 @@ test("exact committed retry finishes pending singleton replacement before succes
598
598
 
599
599
  try {
600
600
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
601
- await assert.rejects(() => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}), /ENOENT/)
601
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/ENOENT/)
602
602
 
603
- assert.equal(daemon.status().activeReleaseId, "v2", "traffic remains durably committed")
604
- assert.equal(daemon.status().generationTransition?.phase, "committed_pending")
605
- assert.notEqual(daemon.status().singletons[0]?.process.state, "running")
603
+ // Traffic remains durably committed.
604
+ expect(daemon.status().activeReleaseId).toBe("v2")
605
+ expect(daemon.status().generationTransition?.phase).toBe("committed_pending")
606
+ expect(daemon.status().singletons[0]?.process.state).not.toBe("running")
606
607
 
607
608
  await fs.mkdir(path.join(fixture.root, "v2"))
608
609
  await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
609
610
 
610
- assert.equal(daemon.status().generationTransition?.phase, "committed")
611
- assert.equal(daemon.status().singletons[0]?.process.state, "running")
612
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "retire:v1", "activate:v2"])
611
+ expect(daemon.status().generationTransition?.phase).toBe("committed")
612
+ expect(daemon.status().singletons[0]?.process.state).toBe("running")
613
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "retire:v1", "activate:v2"])
613
614
  } finally {
614
615
  await daemon.shutdown()
615
616
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -621,12 +622,12 @@ test("first generation is not committed when its activation acknowledgement fail
621
622
  const daemon = await startDaemon(fixture.config)
622
623
 
623
624
  try {
624
- await assert.rejects(() => daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"}), /activate command exited non-zero/)
625
+ await expect(daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})).rejects.toThrow(/activate command exited non-zero/)
625
626
  const status = daemon.status()
626
627
 
627
- assert.equal(status.activeReleaseId, null)
628
- assert.equal(status.generationTransition?.phase, "activating_candidate")
629
- assert.deepEqual(status.releaseReferences.map((reference) => reference.releaseId), ["v1"])
628
+ expect(status.activeReleaseId).toBe(null)
629
+ expect(status.generationTransition?.phase).toBe("activating_candidate")
630
+ expect(status.releaseReferences.map((reference) => reference.releaseId)).toEqual(["v1"])
630
631
  } finally {
631
632
  await daemon.shutdown()
632
633
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -639,21 +640,21 @@ test("retirement acknowledgement failure retains the exact transition, blocks ot
639
640
 
640
641
  try {
641
642
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
642
- await assert.rejects(() => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}), /retirement quiescence failed/)
643
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/retirement quiescence failed/)
643
644
 
644
645
  const failed = daemon.status()
645
646
 
646
- assert.equal(failed.activeReleaseId, "v1")
647
- assert.equal(failed.generationTransition?.phase, "retiring_previous")
648
- assert.match(String(failed.generationTransition?.error), /quiet command exited non-zero/)
649
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1"])
650
- await assert.rejects(() => daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"}), /transition.*v2.*unresolved/i)
647
+ expect(failed.activeReleaseId).toBe("v1")
648
+ expect(failed.generationTransition?.phase).toBe("retiring_previous")
649
+ expect(String(failed.generationTransition?.error)).toMatch(/quiet command exited non-zero/)
650
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1"])
651
+ await expect(daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"})).rejects.toThrow(/transition.*v2.*unresolved/i)
651
652
 
652
653
  await fs.writeFile(fixture.retirementGatePath, "allow\n")
653
654
  await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
654
655
 
655
- assert.equal(daemon.status().activeReleaseId, "v2")
656
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "retire:v1", "activate:v2"])
656
+ expect(daemon.status().activeReleaseId).toBe("v2")
657
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "retire:v1", "activate:v2"])
657
658
  } finally {
658
659
  await daemon.shutdown()
659
660
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -669,41 +670,37 @@ test("candidate activation failure reports restoration failure and exact recover
669
670
  const incumbentCoordinator = daemon.releases.get("v1")?.getProcess("beacon")
670
671
  const reactivate = incumbentCoordinator?.reactivateStrict.bind(incumbentCoordinator)
671
672
 
672
- assert.ok(incumbentCoordinator && reactivate)
673
+ if (!(incumbentCoordinator && reactivate)) throw new Error("Missing required fixture: incumbentCoordinator && reactivate")
673
674
  incumbentCoordinator.reactivateStrict = async () => { throw new Error("incumbent restoration rejected") }
674
- await assert.rejects(
675
- () => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
676
- error => {
677
- assert.ok(error instanceof AggregateError)
678
- assert.match(error.message, /activate command exited non-zero/)
679
- assert.match(error.message, /incumbent v1 restoration failed: incumbent restoration rejected/i)
680
- return true
681
- }
682
- )
675
+ const deployment = daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
676
+
677
+ await expect(deployment).rejects.toBeInstanceOf(AggregateError)
678
+ await expect(deployment).rejects.toMatchObject({message: expect.stringMatching(/activate command exited non-zero/)})
679
+ await expect(deployment).rejects.toMatchObject({message: expect.stringMatching(/incumbent v1 restoration failed: incumbent restoration rejected/i)})
683
680
 
684
681
  const failed = daemon.status()
685
682
 
686
- assert.equal(failed.activeReleaseId, "v1")
687
- assert.equal(failed.generationTransition?.phase, "restoring_previous")
688
- assert.match(String(failed.generationTransition?.activationError), /activate command exited non-zero/)
689
- assert.match(String(failed.generationTransition?.compensationError), /incumbent restoration rejected/)
683
+ expect(failed.activeReleaseId).toBe("v1")
684
+ expect(failed.generationTransition?.phase).toBe("restoring_previous")
685
+ expect(String(failed.generationTransition?.activationError)).toMatch(/activate command exited non-zero/)
686
+ expect(String(failed.generationTransition?.compensationError)).toMatch(/incumbent restoration rejected/)
690
687
  const failedEvents = daemon.eventLog.recent()
691
688
  const activationEvent = failedEvents.find((event) => event.message === "release generation activation failed")
692
689
  const restorationEvent = failedEvents.find((event) => event.message === "release generation compensation restoration failed")
693
690
 
694
- assert.match(String(activationEvent?.data.error), /activate command exited non-zero/)
695
- assert.match(String(restorationEvent?.data.activationError), /activate command exited non-zero/)
696
- assert.match(String(restorationEvent?.data.error), /incumbent restoration rejected/)
697
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "retire:v1", "retire:v2"])
698
- await assert.rejects(() => daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"}), /transition.*v2.*unresolved/i)
691
+ expect(String(activationEvent?.data.error)).toMatch(/activate command exited non-zero/)
692
+ expect(String(restorationEvent?.data.activationError)).toMatch(/activate command exited non-zero/)
693
+ expect(String(restorationEvent?.data.error)).toMatch(/incumbent restoration rejected/)
694
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "retire:v1", "retire:v2"])
695
+ await expect(daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"})).rejects.toThrow(/transition.*v2.*unresolved/i)
699
696
  const failedCandidate = daemon.releases.get("v2")
700
697
 
701
- assert.ok(failedCandidate)
698
+ if (!failedCandidate) throw new Error("Missing required fixture: failedCandidate")
702
699
  await failedCandidate.stop()
703
700
  incumbentCoordinator.reactivateStrict = reactivate
704
701
  await incumbentCoordinator.stop()
705
- assert.equal(failedCandidate.state, "stopped")
706
- assert.equal(incumbentCoordinator.status().state, "stopped")
702
+ expect(failedCandidate.state).toBe("stopped")
703
+ expect(incumbentCoordinator.status().state).toBe("stopped")
707
704
  daemon.config = structuredClone(daemon.config)
708
705
  daemon.config.processes[0].lifecycle.activateTimeoutMs = (daemon.config.processes[0].lifecycle.activateTimeoutMs ?? 30000) + 1
709
706
  const recovery = await sendControlCommand({
@@ -717,13 +714,13 @@ test("candidate activation failure reports restoration failure and exact recover
717
714
  path: fixture.config.control.path
718
715
  })
719
716
 
720
- assert.equal(recovery.recoveryStatus, "recovered")
721
- assert.equal(daemon.status().activeReleaseId, "v1")
722
- assert.equal(daemon.status().generationTransition, undefined)
717
+ expect(recovery.recoveryStatus).toBe("recovered")
718
+ expect(daemon.status().activeReleaseId).toBe("v1")
719
+ expect(daemon.status().generationTransition).toBe(undefined)
723
720
  const persisted = /** @type {{generationTransition?: import("../src/json.js").JsonValue} | undefined} */ (await readState(fixture.statePath))
724
721
 
725
- assert.equal(persisted?.generationTransition, undefined)
726
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "retire:v1", "retire:v2", "activate:v1"])
722
+ expect(persisted?.generationTransition).toBe(undefined)
723
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "retire:v1", "retire:v2", "activate:v1"])
727
724
 
728
725
  const idempotent = await sendControlCommand({
729
726
  command: {
@@ -736,10 +733,9 @@ test("candidate activation failure reports restoration failure and exact recover
736
733
  path: fixture.config.control.path
737
734
  })
738
735
 
739
- assert.equal(idempotent.recoveryStatus, "already_recovered")
736
+ expect(idempotent.recoveryStatus).toBe("already_recovered")
740
737
  await daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"})
741
- await assert.rejects(
742
- () => sendControlCommand({
738
+ await expect(sendControlCommand({
743
739
  command: {
744
740
  command: "recover-generation-transition",
745
741
  previousReleaseId: "v1",
@@ -748,9 +744,7 @@ test("candidate activation failure reports restoration failure and exact recover
748
744
  revision: "v3"
749
745
  },
750
746
  path: fixture.config.control.path
751
- }),
752
- /not a safe failed pre-commit transition/i
753
- )
747
+ })).rejects.toThrow(/not a safe failed pre-commit transition/i)
754
748
  } finally {
755
749
  await daemon.shutdown()
756
750
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -765,12 +759,9 @@ test("explicit recovery stops the exact failed candidate and fences degraded inc
765
759
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
766
760
  const incumbentCoordinator = daemon.releases.get("v1")?.getProcess("beacon")
767
761
 
768
- assert.ok(incumbentCoordinator)
762
+ if (!incumbentCoordinator) throw new Error("Missing required fixture: incumbentCoordinator")
769
763
  incumbentCoordinator.reactivateStrict = async () => { throw new Error("Cannot activate background jobs generation from retired") }
770
- await assert.rejects(
771
- () => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
772
- /Cannot activate background jobs generation from retired/i
773
- )
764
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/Cannot activate background jobs generation from retired/i)
774
765
 
775
766
  const candidate = daemon.releases.get("v2")
776
767
  const transition = daemon.generationTransition
@@ -788,46 +779,49 @@ test("explicit recovery stops the exact failed candidate and fences degraded inc
788
779
  path: fixture.config.control.path
789
780
  })
790
781
 
791
- assert.ok(candidate && transition && incumbentWebPid)
792
- assert.equal(candidate.state, "draining", "ordinary failed compensation leaves the candidate draining")
782
+ if (!(candidate && transition && incumbentWebPid)) throw new Error("Missing required fixture: candidate && transition && incumbentWebPid")
783
+ // Ordinary failed compensation leaves the candidate draining.
784
+ expect(candidate.state).toBe("draining")
793
785
 
794
786
  const retainedCandidateConfig = candidate.config
795
787
 
796
788
  candidate.config = {...candidate.config, releaseRetention: {...candidate.config.releaseRetention, keep: candidate.config.releaseRetention.keep + 1}}
797
- await assert.rejects(() => exactRecovery(), /does not retain its exact path, revision, and config authority/i)
789
+ await expect(exactRecovery()).rejects.toThrow(/does not retain its exact path, revision, and config authority/i)
798
790
  candidate.config = retainedCandidateConfig
799
- await assert.rejects(() => exactRecovery({previousReleaseId: "wrong-v1"}), /refusing stale recovery/i)
800
- await assert.rejects(() => exactRecovery({revision: "wrong-v2"}), /exact same release, path, revision, and config authority/i)
791
+ await expect(exactRecovery({previousReleaseId: "wrong-v1"})).rejects.toThrow(/refusing stale recovery/i)
792
+ await expect(exactRecovery({revision: "wrong-v2"})).rejects.toThrow(/exact same release, path, revision, and config authority/i)
801
793
  transition.phase = "retiring_failed_candidate"
802
- await assert.rejects(() => exactRecovery(), /requires retiring_previous or restoring_previous/i)
794
+ await expect(exactRecovery()).rejects.toThrow(/requires retiring_previous or restoring_previous/i)
803
795
  transition.phase = "restoring_previous"
804
796
  const terminalFailure = transition.compensationError
805
797
 
806
798
  transition.compensationError = "incumbent activation was temporarily unavailable"
807
- await assert.rejects(() => exactRecovery(), /terminal retirement/i)
799
+ await expect(exactRecovery()).rejects.toThrow(/terminal retirement/i)
808
800
  transition.compensationError = terminalFailure
809
801
  await incumbentCoordinator.setLifecycleRole("retired")
810
- assert.equal(incumbentCoordinator.status().lifecycleRole, "retired")
802
+ expect(incumbentCoordinator.status().lifecycleRole).toBe("retired")
811
803
  const checkpoint = daemon.checkpointGenerationTransition.bind(daemon)
812
804
 
813
805
  daemon.checkpointGenerationTransition = async () => { throw new Error("injected checkpoint failure") }
814
- await assert.rejects(() => exactRecovery(), /checkpoint failed: injected checkpoint failure/i)
815
- assert.equal(daemon.generationTransition, transition)
806
+ await expect(exactRecovery()).rejects.toThrow(/checkpoint failed: injected checkpoint failure/i)
807
+ expect(daemon.generationTransition).toBe(transition)
816
808
  daemon.checkpointGenerationTransition = checkpoint
817
809
 
818
810
  const eventsBeforeRecovery = await lifecycleEvents(fixture.lifecycleLogPath)
819
811
  const recovery = await exactRecovery()
820
812
 
821
- assert.equal(recovery.recoveryStatus, "retired_incumbent_accepted")
822
- assert.equal(recovery.jobsStatus, "degraded")
823
- assert.equal(daemon.status().generationTransition?.phase, "degraded_active")
824
- assert.equal(statusRelease(daemon, "v1").processes.find(({id}) => id === "web")?.pid, incumbentWebPid)
825
- assert.equal(await fetchText(daemon, "/release"), "v1")
826
- assert.ok(["draining", "stopped"].includes(candidate.state), "guarded recovery returns before failed-candidate drain completion")
827
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), eventsBeforeRecovery, "recovery must not activate either retained generation")
813
+ expect(recovery.recoveryStatus).toBe("retired_incumbent_accepted")
814
+ expect(recovery.jobsStatus).toBe("degraded")
815
+ expect(daemon.status().generationTransition?.phase).toBe("degraded_active")
816
+ expect(statusRelease(daemon, "v1").processes.find(({id}) => id === "web")?.pid).toBe(incumbentWebPid)
817
+ expect(await fetchText(daemon, "/release")).toBe("v1")
818
+ // Guarded recovery returns before failed-candidate drain completion.
819
+ expect(["draining", "stopped"].includes(candidate.state)).toBe(true)
820
+ // Recovery must not activate either retained generation.
821
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(eventsBeforeRecovery)
828
822
  const persisted = /** @type {{generationTransition?: import("../src/json.js").JsonValue} | undefined} */ (await readState(fixture.statePath))
829
823
 
830
- assert.equal(/** @type {{phase?: string} | undefined} */ (persisted?.generationTransition)?.phase, "degraded_active")
824
+ expect(/** @type {{phase?: string} | undefined} */ (persisted?.generationTransition)?.phase).toBe("degraded_active")
831
825
 
832
826
  transition.phase = "retiring_previous"
833
827
  transition.error = "Release v1 retirement quiescence failed: quiet command exited non-zero with status 1"
@@ -836,8 +830,9 @@ test("explicit recovery stops the exact failed candidate and fences degraded inc
836
830
  await daemon.checkpointGenerationTransition()
837
831
  const legacyRecovery = await exactRecovery()
838
832
 
839
- assert.equal(legacyRecovery.recoveryStatus, "retired_incumbent_accepted")
840
- assert.equal(daemon.status().generationTransition?.phase, "degraded_active", "guarded recovery migrates a legacy terminal retirement fence")
833
+ expect(legacyRecovery.recoveryStatus).toBe("retired_incumbent_accepted")
834
+ // Guarded recovery migrates a legacy terminal retirement fence.
835
+ expect(daemon.status().generationTransition?.phase).toBe("degraded_active")
841
836
 
842
837
  transition.phase = "restoring_previous"
843
838
  transition.compensationError = "Process background-jobs-main is not retained for reactivation"
@@ -845,17 +840,19 @@ test("explicit recovery stops the exact failed candidate and fences degraded inc
845
840
  await daemon.checkpointGenerationTransition()
846
841
  const absentCoordinatorRecovery = await exactRecovery()
847
842
 
848
- assert.equal(absentCoordinatorRecovery.jobsStatus, "degraded")
849
- assert.equal(daemon.status().generationTransition?.phase, "degraded_active", "terminally absent incumbent coordinator remains guarded jobs-degraded authority")
850
- await assert.rejects(() => daemon.deploy({releaseId: "bad-v3", releasePath: fixture.root, revision: "bad-v3"}), /health check failed/i)
851
- assert.equal(daemon.status().generationTransition?.phase, "degraded_active")
852
- assert.equal(statusRelease(daemon, "v1").processes.find(({id}) => id === "web")?.pid, incumbentWebPid)
853
- assert.equal(await fetchText(daemon, "/release"), "v1")
843
+ expect(absentCoordinatorRecovery.jobsStatus).toBe("degraded")
844
+ // Terminally absent incumbent coordinator remains guarded jobs-degraded authority.
845
+ expect(daemon.status().generationTransition?.phase).toBe("degraded_active")
846
+ await expect(daemon.deploy({releaseId: "bad-v3", releasePath: fixture.root, revision: "bad-v3"})).rejects.toThrow(/health check failed/i)
847
+ expect(daemon.status().generationTransition?.phase).toBe("degraded_active")
848
+ expect(statusRelease(daemon, "v1").processes.find(({id}) => id === "web")?.pid).toBe(incumbentWebPid)
849
+ expect(await fetchText(daemon, "/release")).toBe("v1")
854
850
  await daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"})
855
- assert.equal(daemon.status().activeReleaseId, "v3")
856
- assert.equal(daemon.status().generationTransition?.phase, "committed")
857
- assert.equal(await fetchText(daemon, "/release"), "v3")
858
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "retire:v1", "retire:v2", "retire:bad-v3", "activate:v3"], "fresh deployment must not re-retire a degraded incumbent generation")
851
+ expect(daemon.status().activeReleaseId).toBe("v3")
852
+ expect(daemon.status().generationTransition?.phase).toBe("committed")
853
+ expect(await fetchText(daemon, "/release")).toBe("v3")
854
+ // Fresh deployment must not re-retire a degraded incumbent generation.
855
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "retire:v1", "retire:v2", "retire:bad-v3", "activate:v3"])
859
856
  } finally {
860
857
  await daemon.shutdown()
861
858
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -868,23 +865,20 @@ test("candidate activation failure compensates to the incumbent and admits a dif
868
865
 
869
866
  try {
870
867
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
871
- await assert.rejects(
872
- () => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
873
- /activate command exited non-zero.*compensation restored incumbent v1 as authoritative and retired failed candidate v2/i
874
- )
868
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/activate command exited non-zero.*compensation restored incumbent v1 as authoritative and retired failed candidate v2/i)
875
869
 
876
870
  const compensated = daemon.status()
877
871
 
878
- assert.equal(compensated.activeReleaseId, "v1")
879
- assert.equal(compensated.generationTransition, undefined)
880
- assert.equal(await fetchText(daemon, "/release"), "v1")
881
- assert.equal(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "worker")?.state, "running")
882
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "retire:v1", "retire:v2", "activate:v1"])
872
+ expect(compensated.activeReleaseId).toBe("v1")
873
+ expect(compensated.generationTransition).toBe(undefined)
874
+ expect(await fetchText(daemon, "/release")).toBe("v1")
875
+ expect(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "worker")?.state).toBe("running")
876
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "retire:v1", "retire:v2", "activate:v1"])
883
877
 
884
878
  await daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"})
885
879
 
886
- assert.equal(daemon.status().activeReleaseId, "v3")
887
- assert.equal(await fetchText(daemon, "/release"), "v3")
880
+ expect(daemon.status().activeReleaseId).toBe("v3")
881
+ expect(await fetchText(daemon, "/release")).toBe("v3")
888
882
  } finally {
889
883
  await daemon.shutdown()
890
884
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -897,20 +891,17 @@ test("ambiguous candidate activation retires the candidate before reactivating t
897
891
 
898
892
  try {
899
893
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
900
- await assert.rejects(
901
- () => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
902
- /activate command exited non-zero.*compensation restored incumbent v1 as authoritative and retired failed candidate v2/i
903
- )
894
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/activate command exited non-zero.*compensation restored incumbent v1 as authoritative and retired failed candidate v2/i)
904
895
 
905
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), [
896
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual([
906
897
  "activate:v1",
907
898
  "retire:v1",
908
899
  "activate:v2",
909
900
  "retire:v2",
910
901
  "activate:v1"
911
902
  ])
912
- assert.equal(daemon.status().activeReleaseId, "v1")
913
- assert.equal(daemon.status().generationTransition, undefined)
903
+ expect(daemon.status().activeReleaseId).toBe("v1")
904
+ expect(daemon.status().generationTransition).toBe(undefined)
914
905
  } finally {
915
906
  await daemon.shutdown()
916
907
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -923,20 +914,17 @@ test("candidate activation recovery reverses a worker-specific quiet hook before
923
914
 
924
915
  try {
925
916
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
926
- await assert.rejects(
927
- () => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
928
- /compensation restored incumbent v1 as authoritative/i
929
- )
917
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/compensation restored incumbent v1 as authoritative/i)
930
918
 
931
919
  const events = await lifecycleEvents(fixture.lifecycleLogPath)
932
920
  const candidateRetired = events.indexOf("worker-retire:v2")
933
921
  const workerReactivated = events.indexOf("worker-reactivate:v1")
934
922
 
935
- assert.ok(candidateRetired >= 0, JSON.stringify(events))
936
- assert.ok(workerReactivated > candidateRetired, JSON.stringify(events))
937
- assert.equal(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "worker")?.state, "running")
938
- assert.equal(daemon.status().activeReleaseId, "v1")
939
- assert.equal(daemon.status().generationTransition, undefined)
923
+ expect({value: Boolean(candidateRetired >= 0), context: JSON.stringify(events)}).toMatchObject({value: true})
924
+ expect({value: Boolean(workerReactivated > candidateRetired), context: JSON.stringify(events)}).toMatchObject({value: true})
925
+ expect(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "worker")?.state).toBe("running")
926
+ expect(daemon.status().activeReleaseId).toBe("v1")
927
+ expect(daemon.status().generationTransition).toBe(undefined)
940
928
  } finally {
941
929
  await daemon.shutdown()
942
930
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -949,28 +937,22 @@ test("candidate activation recovery keeps the fence when a worker-specific resum
949
937
 
950
938
  try {
951
939
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
952
- await assert.rejects(
953
- () => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
954
- error => {
955
- const failure = /** @type {Error} */ (error)
956
-
957
- assert.match(failure.message, /activate command exited non-zero/)
958
- assert.match(failure.message, /reactivate command exited non-zero/)
959
- return true
960
- }
961
- )
940
+ const deployment = daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
941
+
942
+ await expect(deployment).rejects.toMatchObject({message: expect.stringMatching(/activate command exited non-zero/)})
943
+ await expect(deployment).rejects.toMatchObject({message: expect.stringMatching(/reactivate command exited non-zero/)})
962
944
 
963
945
  const status = daemon.status()
964
946
  const restorationEvent = daemon.eventLog.recent().find((event) => event.message === "release generation compensation restoration failed")
965
947
 
966
- assert.equal(status.activeReleaseId, "v1")
967
- assert.equal(status.generationTransition?.phase, "restoring_previous")
968
- assert.match(String(status.generationTransition?.activationError), /activate command exited non-zero/)
969
- assert.match(String(status.generationTransition?.compensationError), /reactivate command exited non-zero/)
970
- assert.equal(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "worker")?.state, "quiesced")
971
- assert.match(String(restorationEvent?.data.activationError), /activate command exited non-zero/)
972
- assert.match(String(restorationEvent?.data.error), /reactivate command exited non-zero/)
973
- await assert.rejects(() => daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"}), /transition.*v2.*unresolved/i)
948
+ expect(status.activeReleaseId).toBe("v1")
949
+ expect(status.generationTransition?.phase).toBe("restoring_previous")
950
+ expect(String(status.generationTransition?.activationError)).toMatch(/activate command exited non-zero/)
951
+ expect(String(status.generationTransition?.compensationError)).toMatch(/reactivate command exited non-zero/)
952
+ expect(statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "worker")?.state).toBe("quiesced")
953
+ expect(String(restorationEvent?.data.activationError)).toMatch(/activate command exited non-zero/)
954
+ expect(String(restorationEvent?.data.error)).toMatch(/reactivate command exited non-zero/)
955
+ await expect(daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"})).rejects.toThrow(/transition.*v2.*unresolved/i)
974
956
  } finally {
975
957
  await daemon.shutdown()
976
958
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -989,17 +971,14 @@ test("compensation keeps the fence when the cleared checkpoint cannot be persist
989
971
  if (!daemon.generationTransition) throw new Error("cleared checkpoint unavailable")
990
972
  await checkpoint()
991
973
  }
992
- await assert.rejects(
993
- () => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
994
- /activate command exited non-zero.*compensation checkpoint clear failed: cleared checkpoint unavailable/i
995
- )
974
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/activate command exited non-zero.*compensation checkpoint clear failed: cleared checkpoint unavailable/i)
996
975
 
997
- assert.equal(daemon.status().activeReleaseId, "v1")
998
- assert.equal(daemon.status().generationTransition?.phase, "restoring_previous")
976
+ expect(daemon.status().activeReleaseId).toBe("v1")
977
+ expect(daemon.status().generationTransition?.phase).toBe("restoring_previous")
999
978
  const persisted = /** @type {{generationTransition?: {phase?: string}} | undefined} */ (await readState(fixture.statePath))
1000
979
 
1001
- assert.equal(persisted?.generationTransition?.phase, "restoring_previous")
1002
- await assert.rejects(() => daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"}), /transition.*v2.*unresolved/i)
980
+ expect(persisted?.generationTransition?.phase).toBe("restoring_previous")
981
+ await expect(daemon.deploy({releaseId: "v3", releasePath: fixture.root, revision: "v3"})).rejects.toThrow(/transition.*v2.*unresolved/i)
1003
982
 
1004
983
  daemon.checkpointGenerationTransition = checkpoint
1005
984
  const recovery = await sendControlCommand({
@@ -1013,8 +992,8 @@ test("compensation keeps the fence when the cleared checkpoint cannot be persist
1013
992
  path: fixture.config.control.path
1014
993
  })
1015
994
 
1016
- assert.equal(recovery.recoveryStatus, "recovered")
1017
- assert.equal(daemon.status().generationTransition, undefined)
995
+ expect(recovery.recoveryStatus).toBe("recovered")
996
+ expect(daemon.status().generationTransition).toBe(undefined)
1018
997
  } finally {
1019
998
  await daemon.shutdown()
1020
999
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1029,16 +1008,16 @@ test("unresolved generation transition fences stop, restart, and rollback mutati
1029
1008
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
1030
1009
  const incumbentCoordinator = daemon.releases.get("v1")?.getProcess("beacon")
1031
1010
 
1032
- assert.ok(incumbentCoordinator)
1011
+ if (!incumbentCoordinator) throw new Error("Missing required fixture: incumbentCoordinator")
1033
1012
  incumbentCoordinator.reactivateStrict = async () => { throw new Error("incumbent restoration rejected") }
1034
- await assert.rejects(() => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}), /activate command exited non-zero/)
1013
+ await expect(daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})).rejects.toThrow(/activate command exited non-zero/)
1035
1014
 
1036
- await assert.rejects(() => daemon.stopRelease("v2"), /cannot stop.*generation transition.*unresolved/i)
1037
- await assert.rejects(() => daemon.restartProcesses({processId: "beacon"}), /cannot restart.*generation transition.*unresolved/i)
1038
- await assert.rejects(() => daemon.rollback({releaseId: "v2"}), /cannot rollback.*generation transition.*unresolved/i)
1015
+ await expect(daemon.stopRelease("v2")).rejects.toThrow(/cannot stop.*generation transition.*unresolved/i)
1016
+ await expect(daemon.restartProcesses({processId: "beacon"})).rejects.toThrow(/cannot restart.*generation transition.*unresolved/i)
1017
+ await expect(daemon.rollback({releaseId: "v2"})).rejects.toThrow(/cannot rollback.*generation transition.*unresolved/i)
1039
1018
 
1040
- assert.notEqual(statusRelease(daemon, "v2").processes.find((entry) => entry.id === "web")?.state, "stopped")
1041
- assert.equal(await fetchText(daemon, "/release"), "v1")
1019
+ expect(statusRelease(daemon, "v2").processes.find((entry) => entry.id === "web")?.state).not.toBe("stopped")
1020
+ expect(await fetchText(daemon, "/release")).toBe("v1")
1042
1021
  } finally {
1043
1022
  await daemon.shutdown()
1044
1023
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1053,13 +1032,13 @@ test("active generation restores its exact lifecycle role after coordinator auto
1053
1032
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
1054
1033
  const coordinator = statusRelease(daemon, "v1").processes.find((entry) => entry.id === "beacon")
1055
1034
 
1056
- assert.ok(coordinator?.pid)
1035
+ if (!coordinator?.pid) throw new Error("Missing required fixture: coordinator?.pid")
1057
1036
  process.kill(-coordinator.pid, "SIGKILL")
1058
1037
  await waitFor(async () => (await lifecycleEvents(fixture.lifecycleLogPath)).length === 2 && statusRelease(daemon, "v1").processes.find((entry) => entry.id === "beacon")?.state === "running", 3000)
1059
1038
 
1060
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "activate:v1"])
1061
- assert.equal(statusRelease(daemon, "v1").processes.find((entry) => entry.id === "beacon")?.state, "running")
1062
- assert.equal(await fetchText(daemon, "/release"), "v1")
1039
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "activate:v1"])
1040
+ expect(statusRelease(daemon, "v1").processes.find((entry) => entry.id === "beacon")?.state).toBe("running")
1041
+ expect(await fetchText(daemon, "/release")).toBe("v1")
1063
1042
  } finally {
1064
1043
  await daemon.shutdown()
1065
1044
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1077,7 +1056,7 @@ test("failed active-role restoration is loud and never reports the restarted coo
1077
1056
  await fs.rm(fixture.activationGatePath)
1078
1057
  const coordinator = statusRelease(daemon, "v1").processes.find((entry) => entry.id === "beacon")
1079
1058
 
1080
- assert.ok(coordinator?.pid)
1059
+ if (!coordinator?.pid) throw new Error("Missing required fixture: coordinator?.pid")
1081
1060
  process.kill(-coordinator.pid, "SIGKILL")
1082
1061
  await waitFor(() => {
1083
1062
  const status = statusRelease(daemon, "v1").processes.find((entry) => entry.id === "beacon")
@@ -1087,9 +1066,10 @@ test("failed active-role restoration is loud and never reports the restarted coo
1087
1066
 
1088
1067
  const failed = statusRelease(daemon, "v1").processes.find((entry) => entry.id === "beacon")
1089
1068
 
1090
- assert.equal(failed?.lifecycleRole, "active")
1091
- assert.equal(failed?.restarts, 1, "role restoration failure must not create an internal retry loop")
1092
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1"])
1069
+ expect(failed?.lifecycleRole).toBe("active")
1070
+ // Role restoration failure must not create an internal retry loop.
1071
+ expect(failed?.restarts).toBe(1)
1072
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1"])
1093
1073
  } finally {
1094
1074
  await daemon.shutdown()
1095
1075
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1110,25 +1090,27 @@ test("retired generation coordinator remains fenced after exit", async () => {
1110
1090
  const coordinator = statusRelease(daemon, "v1").processes.find((entry) => entry.id === "beacon")
1111
1091
  const coordinatorProcess = daemon.releases.get("v1")?.getProcess("beacon")
1112
1092
 
1113
- assert.ok(coordinator?.pid)
1114
- assert.ok(coordinatorProcess)
1115
- assert.equal(coordinator.lifecycleRole, "retired")
1116
- assert.equal(daemon.guardian?.processes.get("release:v1:beacon"), coordinatorProcess, "retirement refresh must preserve exact guardian event routing")
1093
+ if (!coordinator?.pid) throw new Error("Missing required fixture: coordinator?.pid")
1094
+ if (!coordinatorProcess) throw new Error("Missing required fixture: coordinatorProcess")
1095
+ expect(coordinator.lifecycleRole).toBe("retired")
1096
+ // Retirement refresh must preserve exact guardian event routing.
1097
+ expect(daemon.guardian?.processes.get("release:v1:beacon")).toBe(coordinatorProcess)
1117
1098
  const exited = once(coordinatorProcess, "exit")
1118
1099
 
1119
1100
  process.kill(-coordinator.pid, "SIGKILL")
1120
1101
  const [exit] = await exited
1121
1102
  const stopped = coordinatorProcess.status()
1122
1103
 
1123
- assert.equal(exit.code, null)
1124
- assert.equal(exit.id, "beacon")
1125
- assert.equal(exit.signal, "SIGKILL")
1126
- assert.deepEqual(await lifecycleEvents(fixture.lifecycleLogPath), ["activate:v1", "retire:v1", "activate:v2"])
1127
- assert.equal(stopped.lifecycleRole, "retired")
1128
- assert.equal(stopped.pid, undefined)
1129
- assert.equal(stopped.restarts, 0)
1130
- assert.equal(stopped.state, "stopped")
1131
- assert.equal(coordinatorProcess.restartTimer, undefined, "retired process must not queue a restart")
1104
+ expect(exit.code).toBe(null)
1105
+ expect(exit.id).toBe("beacon")
1106
+ expect(exit.signal).toBe("SIGKILL")
1107
+ expect(await lifecycleEvents(fixture.lifecycleLogPath)).toEqual(["activate:v1", "retire:v1", "activate:v2"])
1108
+ expect(stopped.lifecycleRole).toBe("retired")
1109
+ expect(stopped.pid).toBe(undefined)
1110
+ expect(stopped.restarts).toBe(0)
1111
+ expect(stopped.state).toBe("stopped")
1112
+ // Retired process must not queue a restart.
1113
+ expect(coordinatorProcess.restartTimer).toBe(undefined)
1132
1114
  } finally {
1133
1115
  socket?.close()
1134
1116
  await daemon.shutdown()
@@ -1153,14 +1135,14 @@ test("multiple retired jobs generations keep distinct endpoints and live referen
1153
1135
  const status = daemon.status()
1154
1136
  const generations = ["v1", "v2", "v3"].map((releaseId) => statusRelease(daemon, releaseId))
1155
1137
 
1156
- assert.deepEqual(generations.map((release) => release.state), ["draining", "draining", "active"])
1157
- assert.equal(new Set(generations.map((release) => release.ports.beacon)).size, 3)
1158
- assert.deepEqual(status.releaseReferences.map((reference) => reference.releaseId), ["v1", "v2", "v3"])
1159
- assert.deepEqual(status.releaseReferences.map((reference) => reference.releasePath), ["v1", "v2", "v3"].map((releaseId) => path.join(fixture.root, releaseId)))
1138
+ expect(generations.map((release) => release.state)).toEqual(["draining", "draining", "active"])
1139
+ expect(new Set(generations.map((release) => release.ports.beacon)).size).toBe(3)
1140
+ expect(status.releaseReferences.map((reference) => reference.releaseId)).toEqual(["v1", "v2", "v3"])
1141
+ expect(status.releaseReferences.map((reference) => reference.releasePath)).toEqual(["v1", "v2", "v3"].map((releaseId) => path.join(fixture.root, releaseId)))
1160
1142
 
1161
1143
  for (const socket of sockets.splice(0)) socket.close()
1162
1144
  await waitFor(() => statusRelease(daemon, "v1").state === "stopped" && statusRelease(daemon, "v2").state === "stopped")
1163
- assert.deepEqual(daemon.status().releaseReferences.map((reference) => reference.releaseId), ["v3"])
1145
+ expect(daemon.status().releaseReferences.map((reference) => reference.releaseId)).toEqual(["v3"])
1164
1146
  } finally {
1165
1147
  for (const socket of sockets) socket.close()
1166
1148
  await daemon.shutdown()
@@ -1177,16 +1159,17 @@ test("candidate failure preserves old traffic, jobs generation, endpoint, and re
1177
1159
  const before = statusRelease(daemon, "good")
1178
1160
  const beforeService = before.processes.find((processStatus) => processStatus.id === "beacon")
1179
1161
 
1180
- await assert.rejects(() => daemon.deploy({releaseId: "bad", releasePath: fixture.root, revision: "bad"}), /Health check failed/)
1162
+ await expect(daemon.deploy({releaseId: "bad", releasePath: fixture.root, revision: "bad"})).rejects.toThrow(/Health check failed/)
1181
1163
 
1182
1164
  const after = statusRelease(daemon, "good")
1183
1165
 
1184
- assert.equal(await fetchText(daemon, "/release"), "good")
1185
- assert.equal(after.state, "active")
1186
- assert.equal(after.ports.beacon, before.ports.beacon)
1187
- assert.equal(after.processes.find((processStatus) => processStatus.id === "beacon")?.pid, beforeService?.pid)
1188
- assert.equal((await fs.readFile(fixture.serviceQuietPath, "utf8")).includes("good\n"), false, "candidate cleanup must not quiesce the active generation")
1189
- assert.deepEqual(daemon.status().releaseReferences.map((reference) => reference.releaseId), ["good"])
1166
+ expect(await fetchText(daemon, "/release")).toBe("good")
1167
+ expect(after.state).toBe("active")
1168
+ expect(after.ports.beacon).toBe(before.ports.beacon)
1169
+ expect(after.processes.find((processStatus) => processStatus.id === "beacon")?.pid).toBe(beforeService?.pid)
1170
+ // Candidate cleanup must not quiesce the active generation.
1171
+ expect((await fs.readFile(fixture.serviceQuietPath, "utf8")).includes("good\n")).toBe(false)
1172
+ expect(daemon.status().releaseReferences.map((reference) => reference.releaseId)).toEqual(["good"])
1190
1173
  } finally {
1191
1174
  await daemon.shutdown()
1192
1175
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1206,12 +1189,12 @@ test("handoff-service quiescence failure is visible and leaves the generation al
1206
1189
 
1207
1190
  const retired = statusRelease(daemon, "v1")
1208
1191
 
1209
- assert.equal(retired.state, "draining")
1210
- assert.match(String(retired.retirementError), /quiet command exited non-zero.*23/)
1211
- assert.equal(retired.processes.find((processStatus) => processStatus.id === "beacon")?.state, "stopping")
1212
- assert.notEqual(retired.processes.find((processStatus) => processStatus.id === "worker")?.state, "stopped")
1213
- assert.ok(logs.some((entry) => entry.message === "release retirement quiescence failed" && entry.data?.releaseId === "v1"))
1214
- assert.deepEqual(result.retirement, {error: retired.retirementError, releaseId: "v1", status: "quiescence_failed"})
1192
+ expect(retired.state).toBe("draining")
1193
+ expect(String(retired.retirementError)).toMatch(/quiet command exited non-zero.*23/)
1194
+ expect(retired.processes.find((processStatus) => processStatus.id === "beacon")?.state).toBe("stopping")
1195
+ expect(retired.processes.find((processStatus) => processStatus.id === "worker")?.state).not.toBe("stopped")
1196
+ expect(logs.some((entry) => entry.message === "release retirement quiescence failed" && entry.data?.releaseId === "v1")).toBeTruthy()
1197
+ expect(result.retirement).toEqual({error: retired.retirementError, releaseId: "v1", status: "quiescence_failed"})
1215
1198
  } finally {
1216
1199
  await daemon.shutdown()
1217
1200
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1227,21 +1210,21 @@ test("a replicated companion starts one instance per replica, and restart target
1227
1210
 
1228
1211
  const release = daemon.status().releases.find((candidate) => candidate.state === "active")
1229
1212
 
1230
- assert.ok(release)
1213
+ if (!release) throw new Error("Missing required fixture: release")
1231
1214
 
1232
1215
  const workerIds = release.processes.filter((processStatus) => processStatus.id.startsWith("worker")).map((processStatus) => processStatus.id).sort()
1233
1216
 
1234
- assert.deepEqual(workerIds, ["worker#0", "worker#1", "worker#2"])
1217
+ expect(workerIds).toEqual(["worker#0", "worker#1", "worker#2"])
1235
1218
 
1236
1219
  // A specific replica id restarts only that replica.
1237
1220
  const one = await daemon.restartProcesses({processId: "worker#1"})
1238
1221
 
1239
- assert.deepEqual(one.restarted, ["worker#1"])
1222
+ expect(one.restarted).toEqual(["worker#1"])
1240
1223
 
1241
1224
  // The base id restarts every replica.
1242
1225
  const all = /** @type {string[]} */ ((await daemon.restartProcesses({processId: "worker"})).restarted)
1243
1226
 
1244
- assert.deepEqual([...all].sort(), ["worker#0", "worker#1", "worker#2"])
1227
+ expect([...all].sort()).toEqual(["worker#0", "worker#1", "worker#2"])
1245
1228
  } finally {
1246
1229
  await daemon.shutdown()
1247
1230
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1258,12 +1241,12 @@ test("restart bounces a single process by id", async () => {
1258
1241
  const before = pidsById(daemon.status())
1259
1242
  const result = await daemon.restartProcesses({processId: "beacon"})
1260
1243
 
1261
- assert.deepEqual(result.restarted, ["beacon"])
1244
+ expect(result.restarted).toEqual(["beacon"])
1262
1245
 
1263
1246
  const after = pidsById(daemon.status())
1264
1247
 
1265
- assert.ok(before.beacon && after.beacon, "beacon should have a pid before and after")
1266
- assert.notEqual(after.beacon, before.beacon)
1248
+ expect(before.beacon && after.beacon).toBeTruthy()
1249
+ expect(after.beacon).not.toBe(before.beacon)
1267
1250
  } finally {
1268
1251
  await daemon.shutdown()
1269
1252
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1281,14 +1264,15 @@ test("restart with no selector bounces every non-proxied process but not the pro
1281
1264
  const result = await daemon.restartProcesses()
1282
1265
  const restarted = /** @type {string[]} */ (result.restarted)
1283
1266
 
1284
- assert.deepEqual([...restarted].sort(), ["beacon", "jobs-main", "worker"])
1267
+ expect([...restarted].sort()).toEqual(["beacon", "jobs-main", "worker"])
1285
1268
 
1286
1269
  const after = pidsById(daemon.status())
1287
1270
 
1288
- assert.equal(after.web, before.web, "proxied process should not be restarted")
1289
- assert.notEqual(after.beacon, before.beacon)
1290
- assert.notEqual(after["jobs-main"], before["jobs-main"])
1291
- assert.notEqual(after.worker, before.worker)
1271
+ // Proxied process should not be restarted.
1272
+ expect(after.web).toBe(before.web)
1273
+ expect(after.beacon).not.toBe(before.beacon)
1274
+ expect(after["jobs-main"]).not.toBe(before["jobs-main"])
1275
+ expect(after.worker).not.toBe(before.worker)
1292
1276
  } finally {
1293
1277
  await daemon.shutdown()
1294
1278
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1305,12 +1289,13 @@ test("restart --policy targets only processes with that policy", async () => {
1305
1289
  const before = pidsById(daemon.status())
1306
1290
  const result = await daemon.restartProcesses({policy: "companion"})
1307
1291
 
1308
- assert.deepEqual(result.restarted, ["worker"])
1292
+ expect(result.restarted).toEqual(["worker"])
1309
1293
 
1310
1294
  const after = pidsById(daemon.status())
1311
1295
 
1312
- assert.notEqual(after.worker, before.worker)
1313
- assert.equal(after.beacon, before.beacon, "the service should be left running")
1296
+ expect(after.worker).not.toBe(before.worker)
1297
+ // The service should be left running.
1298
+ expect(after.beacon).toBe(before.beacon)
1314
1299
  } finally {
1315
1300
  await daemon.shutdown()
1316
1301
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1324,9 +1309,9 @@ test("restart refuses the proxied process and reports unknown ids", async () =>
1324
1309
  try {
1325
1310
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
1326
1311
 
1327
- await assert.rejects(() => daemon.restartProcesses({processId: "web"}), /proxied process cannot be restarted/)
1328
- await assert.rejects(() => daemon.restartProcesses({policy: "proxied"}), /proxied process cannot be restarted/)
1329
- await assert.rejects(() => daemon.restartProcesses({processId: "missing"}), /No managed process with id "missing"/)
1312
+ await expect(daemon.restartProcesses({processId: "web"})).rejects.toThrow(/proxied process cannot be restarted/)
1313
+ await expect(daemon.restartProcesses({policy: "proxied"})).rejects.toThrow(/proxied process cannot be restarted/)
1314
+ await expect(daemon.restartProcesses({processId: "missing"})).rejects.toThrow(/No managed process with id "missing"/)
1330
1315
  } finally {
1331
1316
  await daemon.shutdown()
1332
1317
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1343,15 +1328,15 @@ test("restart revives a stopped process instead of erroring", async () => {
1343
1328
  // Simulate the worker having exited (e.g. crashed and exhausted its restart budget).
1344
1329
  const worker = daemon.activeRelease?.getProcess("worker")
1345
1330
 
1346
- assert.ok(worker, "worker process should exist")
1331
+ if (!worker) throw new Error("worker process should exist")
1347
1332
  await worker.stop()
1348
- assert.equal(worker.status().state, "stopped")
1333
+ expect(worker.status().state).toBe("stopped")
1349
1334
 
1350
1335
  const result = await daemon.restartProcesses({processId: "worker"})
1351
1336
 
1352
- assert.deepEqual(result.restarted, ["worker"])
1353
- assert.equal(worker.status().state, "running")
1354
- assert.ok(worker.status().pid)
1337
+ expect(result.restarted).toEqual(["worker"])
1338
+ expect(worker.status().state).toBe("running")
1339
+ expect(worker.status().pid).toBeTruthy()
1355
1340
  } finally {
1356
1341
  await daemon.shutdown()
1357
1342
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1371,8 +1356,8 @@ test("the restart control command bounces a process over the socket", async () =
1371
1356
  path: fixture.config.control.path
1372
1357
  })
1373
1358
 
1374
- assert.deepEqual(response.restarted, ["beacon"])
1375
- assert.notEqual(pidsById(daemon.status()).beacon, before.beacon)
1359
+ expect(response.restarted).toEqual(["beacon"])
1360
+ expect(pidsById(daemon.status()).beacon).not.toBe(before.beacon)
1376
1361
  } finally {
1377
1362
  await daemon.shutdown()
1378
1363
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1388,15 +1373,15 @@ test("status and events distinguish deploy starts from manual restarts", async (
1388
1373
 
1389
1374
  const afterDeploy = daemon.status().services.find((service) => service.id === "beacon")
1390
1375
 
1391
- assert.ok(afterDeploy)
1392
- assert.equal(afterDeploy.process.lastStartReason, "deploy")
1376
+ if (!afterDeploy) throw new Error("Missing required fixture: afterDeploy")
1377
+ expect(afterDeploy.process.lastStartReason).toBe("deploy")
1393
1378
 
1394
1379
  await daemon.restartProcesses({processId: "beacon"})
1395
1380
 
1396
1381
  const afterRestart = daemon.status().services.find((service) => service.id === "beacon")
1397
1382
 
1398
- assert.ok(afterRestart)
1399
- assert.equal(afterRestart.process.lastStartReason, "manual")
1383
+ if (!afterRestart) throw new Error("Missing required fixture: afterRestart")
1384
+ expect(afterRestart.process.lastStartReason).toBe("manual")
1400
1385
 
1401
1386
  const events = /** @type {import("../src/event-log.js").DaemonEvent[]} */ ((await sendControlCommand({
1402
1387
  command: {command: "events"},
@@ -1404,8 +1389,8 @@ test("status and events distinguish deploy starts from manual restarts", async (
1404
1389
  })).events)
1405
1390
  const startReasons = events.filter((event) => event.message === "process started").map((event) => event.data.reason)
1406
1391
 
1407
- assert.ok(startReasons.includes("deploy"), JSON.stringify(startReasons))
1408
- assert.ok(startReasons.includes("manual"), JSON.stringify(startReasons))
1392
+ expect({value: Boolean(startReasons.includes("deploy")), context: JSON.stringify(startReasons)}).toMatchObject({value: true})
1393
+ expect({value: Boolean(startReasons.includes("manual")), context: JSON.stringify(startReasons)}).toMatchObject({value: true})
1409
1394
  } finally {
1410
1395
  await daemon.shutdown()
1411
1396
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1434,7 +1419,8 @@ test("persists daemon state to statePath and removes it on a clean shutdown", as
1434
1419
  await fs.rm(fixture.root, {force: true, recursive: true})
1435
1420
  }
1436
1421
 
1437
- assert.equal(stateAfterShutdown, undefined, "state file removed on clean shutdown")
1422
+ // State file removed on clean shutdown.
1423
+ expect(stateAfterShutdown).toBe(undefined)
1438
1424
  })
1439
1425
 
1440
1426
  test("persisted daemon state excludes process commands, environment values, and output", async () => {
@@ -1442,7 +1428,7 @@ test("persisted daemon state excludes process commands, environment values, and
1442
1428
  const fixture = await createFixture({persistState: true})
1443
1429
  const web = fixture.config.processes.find((processConfig) => processConfig.id === "web")
1444
1430
 
1445
- assert.ok(web)
1431
+ if (!web) throw new Error("Missing required fixture: web")
1446
1432
  web.env.ROLLBRIDGE_TEST_SECRET = secret
1447
1433
  web.command = `${JSON.stringify(process.execPath)} -e ${JSON.stringify(`console.log(process.env.ROLLBRIDGE_TEST_SECRET); import(${JSON.stringify(pathToFileURL(dummyAppPath).href)})`)}`
1448
1434
 
@@ -1452,20 +1438,21 @@ test("persisted daemon state excludes process commands, environment values, and
1452
1438
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
1453
1439
  const webProcess = daemon.activeRelease?.getProcess("web")
1454
1440
 
1455
- assert.ok(webProcess)
1441
+ if (!webProcess) throw new Error("Missing required fixture: webProcess")
1456
1442
  await recordedLogLine(webProcess, secret)
1457
- assert.ok(webProcess.status().logs.some((entry) => entry.line === secret), "secret output must be retained before persistence")
1443
+ // Secret output must be retained before persistence.
1444
+ expect(webProcess.status().logs.some((entry) => entry.line === secret)).toBe(true)
1458
1445
 
1459
1446
  daemon.persistState()
1460
1447
  await waitFor(async () => (await fs.readFile(fixture.statePath, "utf8")).includes('"activeReleaseId": "v1"'))
1461
1448
 
1462
1449
  const persisted = await fs.readFile(fixture.statePath, "utf8")
1463
1450
 
1464
- assert.doesNotMatch(persisted, /state-secret-value/)
1465
- assert.doesNotMatch(persisted, /ROLLBRIDGE_TEST_SECRET/)
1466
- assert.doesNotMatch(persisted, /"command"/)
1467
- assert.doesNotMatch(persisted, /"logs"/)
1468
- assert.deepEqual(liveProcesses(JSON.parse(persisted), () => true).map(({id, releaseId}) => ({id, releaseId})), [{id: "web", releaseId: "v1"}])
1451
+ expect(persisted).not.toMatch(/state-secret-value/)
1452
+ expect(persisted).not.toMatch(/ROLLBRIDGE_TEST_SECRET/)
1453
+ expect(persisted).not.toMatch(/"command"/)
1454
+ expect(persisted).not.toMatch(/"logs"/)
1455
+ expect(liveProcesses(JSON.parse(persisted), () => true).map(({id, releaseId}) => ({id, releaseId}))).toEqual([{id: "web", releaseId: "v1"}])
1469
1456
  } finally {
1470
1457
  await daemon.shutdown()
1471
1458
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1482,7 +1469,8 @@ test("a clean shutdown clears the state file even when a persist write is in fli
1482
1469
  // Shut down immediately — the deploy's fire-and-forget persist may still be in flight.
1483
1470
  await daemon.shutdown()
1484
1471
 
1485
- assert.equal(await readState(fixture.statePath), undefined, "state file must not be recreated by an in-flight write")
1472
+ // State file must not be recreated by an in-flight write.
1473
+ expect(await readState(fixture.statePath)).toBe(undefined)
1486
1474
  } finally {
1487
1475
  if (!daemon.stopping) await daemon.shutdown()
1488
1476
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1519,7 +1507,7 @@ test("reports orphaned managed processes from a previous daemon's state", async
1519
1507
 
1520
1508
  await daemon.reportOrphans()
1521
1509
 
1522
- assert.ok(logs.some((entry) => entry.message === "orphaned managed process detected" && entry.data.pid === leftover.pid), JSON.stringify(logs))
1510
+ expect({value: Boolean(logs.some((entry) => entry.message === "orphaned managed process detected" && entry.data.pid === leftover.pid)), context: JSON.stringify(logs)}).toMatchObject({value: true})
1523
1511
 
1524
1512
  // A dead pid is not reported.
1525
1513
  logs.length = 0
@@ -1531,7 +1519,7 @@ test("reports orphaned managed processes from a previous daemon's state", async
1531
1519
  })
1532
1520
  await daemon.reportOrphans()
1533
1521
 
1534
- assert.ok(!logs.some((entry) => entry.message === "orphaned managed process detected"))
1522
+ expect(!logs.some((entry) => entry.message === "orphaned managed process detected")).toBeTruthy()
1535
1523
  } finally {
1536
1524
  leftover.kill("SIGKILL")
1537
1525
  await fs.rm(dir, {force: true, recursive: true})
@@ -1566,16 +1554,16 @@ test("status surfaces still-alive orphaned processes from a previous daemon and
1566
1554
  await daemon.reportOrphans()
1567
1555
 
1568
1556
  // status reflects the still-running child even though the daemon cannot re-manage it.
1569
- assert.deepEqual(daemon.status().orphans, [{id: "worker", pid: leftover.pid, releaseId: "v1"}])
1557
+ expect(daemon.status().orphans).toEqual([{id: "worker", pid: leftover.pid, releaseId: "v1"}])
1570
1558
 
1571
1559
  // Once the leftover is stopped, status re-checks liveness and drops it.
1572
1560
  leftover.kill("SIGKILL")
1573
1561
  await waitFor(() => daemon.status().orphans.length === 0)
1574
- assert.deepEqual(daemon.status().orphans, [])
1562
+ expect(daemon.status().orphans).toEqual([])
1575
1563
 
1576
1564
  // The dead entry is pruned from the underlying list, not merely filtered, so a recycled pid
1577
1565
  // can't resurrect a cleared orphan.
1578
- assert.deepEqual(daemon.orphans, [])
1566
+ expect(daemon.orphans).toEqual([])
1579
1567
  } finally {
1580
1568
  leftover.kill("SIGKILL")
1581
1569
  await fs.rm(dir, {force: true, recursive: true})
@@ -1596,14 +1584,14 @@ test("the daemon records a structured event history served by the events command
1596
1584
  const events = /** @type {import("../src/event-log.js").DaemonEvent[]} */ (response.events)
1597
1585
  const messages = events.map((event) => event.message)
1598
1586
 
1599
- assert.ok(messages.includes("deploy starting"), JSON.stringify(messages))
1600
- assert.ok(messages.includes("traffic switched"), JSON.stringify(messages))
1587
+ expect({value: Boolean(messages.includes("deploy starting")), context: JSON.stringify(messages)}).toMatchObject({value: true})
1588
+ expect({value: Boolean(messages.includes("traffic switched")), context: JSON.stringify(messages)}).toMatchObject({value: true})
1601
1589
 
1602
1590
  const switched = events.find((event) => event.message === "traffic switched")
1603
1591
 
1604
- assert.ok(switched)
1605
- assert.equal(switched.data.releaseId, "v1")
1606
- assert.match(switched.at, /^\d{4}-\d{2}-\d{2}T.*Z$/)
1592
+ if (!switched) throw new Error("Missing required fixture: switched")
1593
+ expect(switched.data.releaseId).toBe("v1")
1594
+ expect(switched.at).toMatch(/^\d{4}-\d{2}-\d{2}T.*Z$/)
1607
1595
  } finally {
1608
1596
  await daemon.shutdown()
1609
1597
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1618,32 +1606,32 @@ test("the events command honors --limit and records failed commands", async () =
1618
1606
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
1619
1607
 
1620
1608
  // An unknown command is rejected and recorded as a "command failed" event.
1621
- await assert.rejects(() => sendControlCommand({
1609
+ await expect(sendControlCommand({
1622
1610
  command: {command: "bogus"},
1623
1611
  path: fixture.config.control.path
1624
- }))
1612
+ })).rejects.toThrow()
1625
1613
 
1626
1614
  const all = /** @type {import("../src/event-log.js").DaemonEvent[]} */ ((await sendControlCommand({
1627
1615
  command: {command: "events"},
1628
1616
  path: fixture.config.control.path
1629
1617
  })).events)
1630
1618
 
1631
- assert.ok(all.some((event) => event.message === "command failed"))
1619
+ expect(all.some((event) => event.message === "command failed")).toBeTruthy()
1632
1620
 
1633
1621
  const limited = /** @type {import("../src/event-log.js").DaemonEvent[]} */ ((await sendControlCommand({
1634
1622
  command: {command: "events", limit: 1},
1635
1623
  path: fixture.config.control.path
1636
1624
  })).events)
1637
1625
 
1638
- assert.equal(limited.length, 1)
1639
- assert.deepEqual(limited[0], all[all.length - 1])
1626
+ expect(limited.length).toBe(1)
1627
+ expect(limited[0]).toEqual(all[all.length - 1])
1640
1628
  } finally {
1641
1629
  await daemon.shutdown()
1642
1630
  await fs.rm(fixture.root, {force: true, recursive: true})
1643
1631
  }
1644
1632
  })
1645
1633
 
1646
- test("a process over its memory limit is restarted with reason memory", {skip: process.platform !== "linux" && "requires /proc (Linux)"}, async () => {
1634
+ linuxTest("a process over its memory limit is restarted with reason memory", async () => {
1647
1635
  const limitBytes = 64 * 1024 * 1024
1648
1636
  const fixture = await createFixture({memoryLimitBytes: limitBytes})
1649
1637
  const daemon = await startDaemon(fixture.config)
@@ -1656,17 +1644,17 @@ test("a process over its memory limit is restarted with reason memory", {skip: p
1656
1644
 
1657
1645
  const hog = activeProcessStatus(daemon, "hog")
1658
1646
 
1659
- assert.ok(hog, "hog process should be present")
1660
- assert.ok(hog.memoryRestarts >= 1, `expected a memory restart, got ${hog.memoryRestarts}`)
1661
- assert.equal(hog.lastStartReason, "memory")
1662
- assert.equal(typeof hog.lastMemoryRestartAt, "string")
1647
+ if (!hog) throw new Error("hog process should be present")
1648
+ expect({value: Boolean(hog.memoryRestarts >= 1), context: `expected a memory restart, got ${hog.memoryRestarts}`}).toMatchObject({value: true})
1649
+ expect(hog.lastStartReason).toBe("memory")
1650
+ expect(typeof hog.lastMemoryRestartAt).toBe("string")
1663
1651
 
1664
1652
  // Keep the replacement alive long enough to observe its next monitor sample. The fixture
1665
1653
  // remains over the configured limit after every launch, otherwise it can restart again and
1666
1654
  // clear rssBytes/children before this polling loop observes them on slower CI runners.
1667
1655
  const hogProcess = daemon.activeRelease?.processes.get("hog")
1668
1656
 
1669
- assert.ok(hogProcess?.memory)
1657
+ if (!hogProcess?.memory) throw new Error("Missing required fixture: hogProcess?.memory")
1670
1658
  hogProcess.memory.limitBytes = Number.MAX_SAFE_INTEGER
1671
1659
 
1672
1660
  // rssBytes is sampled on the monitor's interval; wait for a measurement of the running process.
@@ -1679,9 +1667,9 @@ test("a process over its memory limit is restarted with reason memory", {skip: p
1679
1667
  // The same monitor sample reports the process tree.
1680
1668
  const monitored = activeProcessStatus(daemon, "hog")
1681
1669
 
1682
- assert.ok(monitored)
1683
- assert.ok(monitored.children.length >= 1, "status should include the process tree")
1684
- assert.ok(monitored.children.some((child) => typeof child.rssBytes === "number" && child.rssBytes > 0))
1670
+ if (!monitored) throw new Error("Missing required fixture: monitored")
1671
+ expect(monitored.children.length >= 1).toBe(true)
1672
+ expect(monitored.children.some((child) => typeof child.rssBytes === "number" && child.rssBytes > 0)).toBeTruthy()
1685
1673
  } finally {
1686
1674
  await daemon.shutdown()
1687
1675
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1696,14 +1684,14 @@ test("rollback re-activates the previous release and switches traffic back", asy
1696
1684
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
1697
1685
  await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
1698
1686
 
1699
- assert.equal(await fetchText(daemon, "/release"), "v2")
1687
+ expect(await fetchText(daemon, "/release")).toBe("v2")
1700
1688
 
1701
1689
  const result = await daemon.rollback()
1702
1690
 
1703
- assert.equal(result.activeReleaseId, "v1")
1704
- assert.equal(result.previousReleaseId, "v2")
1705
- assert.equal(daemon.status().activeReleaseId, "v1")
1706
- assert.equal(await fetchText(daemon, "/release"), "v1")
1691
+ expect(result.activeReleaseId).toBe("v1")
1692
+ expect(result.previousReleaseId).toBe("v2")
1693
+ expect(daemon.status().activeReleaseId).toBe("v1")
1694
+ expect(await fetchText(daemon, "/release")).toBe("v1")
1707
1695
  } finally {
1708
1696
  await daemon.shutdown()
1709
1697
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1721,8 +1709,8 @@ test("rollback --release-id targets a specific retained release", async () => {
1721
1709
 
1722
1710
  const result = await daemon.rollback({releaseId: "v1"})
1723
1711
 
1724
- assert.equal(result.activeReleaseId, "v1")
1725
- assert.equal(await fetchText(daemon, "/release"), "v1")
1712
+ expect(result.activeReleaseId).toBe("v1")
1713
+ expect(await fetchText(daemon, "/release")).toBe("v1")
1726
1714
  } finally {
1727
1715
  await daemon.shutdown()
1728
1716
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1736,9 +1724,9 @@ test("rollback rejects no-previous, unknown, and already-active targets", async
1736
1724
  try {
1737
1725
  await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
1738
1726
 
1739
- await assert.rejects(() => daemon.rollback(), /No previous release/)
1740
- await assert.rejects(() => daemon.rollback({releaseId: "v1"}), /already active/)
1741
- await assert.rejects(() => daemon.rollback({releaseId: "nope"}), /No retained release "nope"/)
1727
+ await expect(daemon.rollback()).rejects.toThrow(/No previous release/)
1728
+ await expect(daemon.rollback({releaseId: "v1"})).rejects.toThrow(/already active/)
1729
+ await expect(daemon.rollback({releaseId: "nope"})).rejects.toThrow(/No retained release "nope"/)
1742
1730
  } finally {
1743
1731
  await daemon.shutdown()
1744
1732
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1760,17 +1748,17 @@ test("rollback to a still-draining release stops the old instance instead of orp
1760
1748
 
1761
1749
  const draining = statusRelease(daemon, "v1")
1762
1750
 
1763
- assert.equal(draining.state, "draining")
1751
+ expect(draining.state).toBe("draining")
1764
1752
 
1765
1753
  const oldWebPid = draining.processes.find((processStatus) => processStatus.id === "web")?.pid
1766
1754
 
1767
- assert.ok(oldWebPid, "the draining release should have a running web process")
1755
+ if (!oldWebPid) throw new Error("the draining release should have a running web process")
1768
1756
 
1769
1757
  await daemon.rollback({releaseId: "v1"})
1770
1758
 
1771
- assert.equal(daemon.status().activeReleaseId, "v1")
1759
+ expect(daemon.status().activeReleaseId).toBe("v1")
1772
1760
  // The old draining instance was stopped before its id was reused, so its process is gone.
1773
- assert.throws(() => process.kill(/** @type {number} */ (oldWebPid), 0), /ESRCH/)
1761
+ await expect(() => process.kill(/** @type {number} */ (oldWebPid), 0)).toThrow(/ESRCH/)
1774
1762
  } finally {
1775
1763
  if (socket) socket.close()
1776
1764
  await daemon.shutdown()
@@ -1791,8 +1779,8 @@ test("the rollback control command switches traffic over the socket", async () =
1791
1779
  path: fixture.config.control.path
1792
1780
  })
1793
1781
 
1794
- assert.equal(response.activeReleaseId, "v1")
1795
- assert.equal(await fetchText(daemon, "/release"), "v1")
1782
+ expect(response.activeReleaseId).toBe("v1")
1783
+ expect(await fetchText(daemon, "/release")).toBe("v1")
1796
1784
  } finally {
1797
1785
  await daemon.shutdown()
1798
1786
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1819,8 +1807,8 @@ test("control socket accepts deploy and status commands", async () => {
1819
1807
  path: fixture.config.control.path
1820
1808
  })
1821
1809
 
1822
- assert.equal(status.activeReleaseId, "control-v1")
1823
- assert.equal(await fetchText(daemon, "/release"), "control-v1")
1810
+ expect(status.activeReleaseId).toBe("control-v1")
1811
+ expect(await fetchText(daemon, "/release")).toBe("control-v1")
1824
1812
  } finally {
1825
1813
  await daemon.shutdown()
1826
1814
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -1836,28 +1824,23 @@ test("starting a second daemon on a live control socket reports the running daem
1836
1824
 
1837
1825
  const second = new RollbridgeDaemon({config: fixture.config, logger: () => {}})
1838
1826
 
1839
- await assert.rejects(
1840
- () => second.prepareControlSocketPath(),
1841
- (error) => {
1842
- assert.ok(error instanceof Error)
1843
- assert.match(error.message, /A Rollbridge daemon for application "rollbridge-test" is already running/)
1844
- assert.match(error.message, /active release: v1/)
1845
- assert.match(error.message, /rollbridge shutdown/)
1827
+ const preparation = second.prepareControlSocketPath()
1846
1828
 
1847
- return true
1848
- }
1849
- )
1829
+ await expect(preparation).rejects.toBeInstanceOf(Error)
1830
+ await expect(preparation).rejects.toMatchObject({message: expect.stringMatching(/A Rollbridge daemon for application "rollbridge-test" is already running/)})
1831
+ await expect(preparation).rejects.toMatchObject({message: expect.stringMatching(/active release: v1/)})
1832
+ await expect(preparation).rejects.toMatchObject({message: expect.stringMatching(/rollbridge shutdown/)})
1850
1833
 
1851
1834
  // The original daemon keeps its socket and still answers control commands.
1852
1835
  const status = await sendControlCommand({command: {command: "status"}, path: fixture.config.control.path})
1853
- assert.equal(status.application, "rollbridge-test")
1836
+ expect(status.application).toBe("rollbridge-test")
1854
1837
  } finally {
1855
1838
  await daemon.shutdown()
1856
1839
  await fs.rm(fixture.root, {force: true, recursive: true})
1857
1840
  }
1858
1841
  })
1859
1842
 
1860
- test("the daemon applies control.owner and control.group to the bound socket", {skip: process.platform !== "linux" && "requires POSIX chown"}, async () => {
1843
+ linuxTest("the daemon applies control.owner and control.group to the bound socket", async () => {
1861
1844
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-test-"))
1862
1845
  const socketPath = path.join(root, "rollbridge.sock")
1863
1846
  const {uid, username} = os.userInfo()
@@ -1877,8 +1860,8 @@ test("the daemon applies control.owner and control.group to the bound socket", {
1877
1860
 
1878
1861
  const stats = await fs.stat(socketPath)
1879
1862
 
1880
- assert.equal(stats.uid, uid)
1881
- assert.equal(stats.gid, gid)
1863
+ expect(stats.uid).toBe(uid)
1864
+ expect(stats.gid).toBe(gid)
1882
1865
  } finally {
1883
1866
  await daemon.shutdown()
1884
1867
  await fs.rm(root, {force: true, recursive: true})
@@ -1907,10 +1890,7 @@ test("a control socket held by a non-Rollbridge process reports a generic confli
1907
1890
  const daemon = new RollbridgeDaemon({config, logger: () => {}})
1908
1891
 
1909
1892
  try {
1910
- await assert.rejects(
1911
- () => daemon.prepareControlSocketPath(),
1912
- /The control socket .* is already in use by another process/
1913
- )
1893
+ await expect(daemon.prepareControlSocketPath()).rejects.toThrow(/The control socket .* is already in use by another process/)
1914
1894
  } finally {
1915
1895
  for (const socket of connections) socket.destroy()
1916
1896
  await new Promise((resolve) => stranger.close(() => resolve(undefined)))
@@ -1934,7 +1914,7 @@ test("applies the configured control socket permission mode", async () => {
1934
1914
  try {
1935
1915
  const stats = await fs.stat(socketPath)
1936
1916
 
1937
- assert.equal(stats.mode & 0o777, 0o660)
1917
+ expect(stats.mode & 0o777).toBe(0o660)
1938
1918
  } finally {
1939
1919
  await daemon.shutdown()
1940
1920
  await fs.rm(root, {force: true, recursive: true})
@@ -1974,10 +1954,10 @@ test("deploy can ensure the daemon before sending the release command", async ()
1974
1954
 
1975
1955
  const proxy = /** @type {{port: number}} */ (status.proxy)
1976
1956
 
1977
- assert.equal(status.activeReleaseId, "ensured-v1")
1978
- assert.equal(status.bootstrap, undefined)
1979
- assert.match(await fs.readFile(pidPath, "utf8"), /\d+/)
1980
- assert.equal(await fetchTextFromPort(proxy.port, "/release"), "ensured-v1")
1957
+ expect(status.activeReleaseId).toBe("ensured-v1")
1958
+ expect(status.bootstrap).toBe(undefined)
1959
+ expect(await fs.readFile(pidPath, "utf8")).toMatch(/\d+/)
1960
+ expect(await fetchTextFromPort(proxy.port, "/release")).toBe("ensured-v1")
1981
1961
  } finally {
1982
1962
  try {
1983
1963
  await sendControlCommand({
@@ -2156,7 +2136,7 @@ async function fetchText(daemon, pathName) {
2156
2136
  async function fetchTextFromPort(port, pathName) {
2157
2137
  const response = await fetch(`http://127.0.0.1:${port}${pathName}`)
2158
2138
 
2159
- assert.equal(response.status, 200)
2139
+ expect(response.status).toBe(200)
2160
2140
 
2161
2141
  return (await response.text()).trim()
2162
2142
  }
@@ -2185,7 +2165,7 @@ function statusRelease(daemon, releaseId) {
2185
2165
  const status = daemon.status()
2186
2166
  const release = status.releases.find((candidate) => candidate.releaseId === releaseId)
2187
2167
 
2188
- assert.ok(release, `Release ${releaseId} should be present`)
2168
+ if (!release) throw new Error(`Release ${releaseId} should be present`)
2189
2169
 
2190
2170
  return release
2191
2171
  }
@@ -2305,3 +2285,4 @@ function activeProcessStatus(daemon, processId) {
2305
2285
 
2306
2286
  return release ? release.processes.find((processStatus) => processStatus.id === processId) : undefined
2307
2287
  }
2288
+ })