rollbridge 0.1.49 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +5 -0
  3. package/changelog.d/20260909120000-velocious-testing.md +1 -0
  4. package/docs/cli.md +7 -1
  5. package/docs/generation-deployment-contract.md +9 -0
  6. package/eslint.config.js +8 -0
  7. package/package.json +3 -2
  8. package/src/cli.js +10 -2
  9. package/src/daemon.js +102 -12
  10. package/src/process-guardian.js +5 -1
  11. package/src/release-group.js +48 -1
  12. package/test/completion.test.js +18 -16
  13. package/test/config-examples.test.js +16 -17
  14. package/test/config-path.test.js +10 -11
  15. package/test/config-validation.test.js +163 -167
  16. package/test/control-protocol.test.js +75 -14
  17. package/test/daemon-bootstrap.test.js +104 -104
  18. package/test/daemon-runtime.test.js +17 -26
  19. package/test/doctor.test.js +51 -49
  20. package/test/event-log.test.js +13 -11
  21. package/test/guardian-client.test.js +160 -145
  22. package/test/health.test.js +6 -4
  23. package/test/logs.test.js +23 -17
  24. package/test/managed-process.test.js +96 -91
  25. package/test/owner-recovery.test.js +254 -239
  26. package/test/owner-replacement.test.js +228 -223
  27. package/test/package-metadata.test.js +48 -39
  28. package/test/port-allocator.test.js +13 -16
  29. package/test/predeploy-cleanup.test.js +12 -10
  30. package/test/process-memory.test.js +17 -15
  31. package/test/proxy.test.js +10 -8
  32. package/test/recover.test.js +30 -23
  33. package/test/release-group.test.js +16 -17
  34. package/test/release-retention.test.js +10 -8
  35. package/test/release-runtime-retention.test.js +31 -39
  36. package/test/rollbridge.test.js +388 -395
  37. package/test/shutdown-completion.test.js +51 -51
  38. package/test/state-store.test.js +10 -8
  39. package/test/system-ids.test.js +15 -13
@@ -1,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} from "node:url"
12
11
  import {normalizeConfig} from "../src/config.js"
13
12
  import {openControlSession, sendControlCommand} from "../src/control-client.js"
@@ -16,6 +15,8 @@ import GuardianClient from "../src/guardian-client.js"
16
15
  import {findAvailablePort} from "../src/port-allocator.js"
17
16
  import {waitForProcessExit} from "./support/process.js"
18
17
 
18
+ describe("owner-replacement", () => {
19
+
19
20
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
20
21
  const binPath = path.join(repoRoot, "bin", "rollbridge")
21
22
  const dummyAppPath = path.join(repoRoot, "test", "fixtures", "dummy-app.js")
@@ -43,7 +44,7 @@ test("partial owner-replacement guardian crosses the authenticated legacy bridge
43
44
  await writeConfig(configPath, config({controlPath: socketPath, extraCompanion: false, statePath}))
44
45
  owner = spawn(process.execPath, [binPath, "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
45
46
  await waitForLog(owner, "control socket listening")
46
- assert.ok(owner.pid)
47
+ if (!owner.pid) throw new Error("Missing required fixture: owner.pid")
47
48
  await fs.writeFile(daemonPidPath, `${owner.pid}\n`)
48
49
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath, revision: "v1"}, path: socketPath})
49
50
  const before = await sendControlCommand({command: {command: "status"}, path: socketPath})
@@ -70,15 +71,16 @@ test("partial owner-replacement guardian crosses the authenticated legacy bridge
70
71
  const ownerExit = once(owner, "exit")
71
72
  const ensured = await runEnsureDaemon({configPath, daemonPidPath, logPath: path.join(root, "candidate.log"), packagePath, runtimePath})
72
73
 
73
- assert.equal(ensured.code, 0, `${ensured.stderr}\n${await fs.readFile(path.join(root, "candidate.log"), "utf8")}`)
74
- assert.deepEqual(await ownerExit, [null, "SIGKILL"], "the exact authenticated incumbent boundary is crossed once")
74
+ expect({value: ensured.code, context: `${ensured.stderr}\n${await fs.readFile(path.join(root, "candidate.log"), "utf8")}`}).toMatchObject({value: 0})
75
+ // The exact authenticated incumbent boundary is crossed once.
76
+ expect(await ownerExit).toEqual([null, "SIGKILL"])
75
77
  await retainedClosed
76
78
  const status = await sendControlCommand({command: {command: "status"}, path: socketPath})
77
79
 
78
- assert.equal(status.activeReleaseId, "v1")
79
- assert.equal(releaseProcessPid(status, "v1", "worker"), workerPid)
80
- assert.equal(releaseProcessPid(status, "v1", "web"), webPid)
81
- assert.deepEqual(status.ownerTransition, {
80
+ expect(status.activeReleaseId).toBe("v1")
81
+ expect(releaseProcessPid(status, "v1", "worker")).toBe(workerPid)
82
+ expect(releaseProcessPid(status, "v1", "web")).toBe(webPid)
83
+ expect(status.ownerTransition).toEqual({
82
84
  disruptive: true,
83
85
  mode: "legacy-first-upgrade",
84
86
  reason: "retained guardian and daemon lacked atomic replacement protocol"
@@ -132,7 +134,7 @@ test("partial guardian replacement remains fenced through coordinator reconstruc
132
134
  await writeConfig(configPath, config({controlPath: socketPath, extraCompanion: false, statePath}))
133
135
  owner = spawn(process.execPath, [binPath, "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
134
136
  await waitForLog(owner, "control socket listening")
135
- assert.ok(owner.pid)
137
+ if (!owner.pid) throw new Error("Missing required fixture: owner.pid")
136
138
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath, revision: "v1"}, path: socketPath})
137
139
  const state = JSON.parse(await fs.readFile(statePath, "utf8"))
138
140
 
@@ -160,7 +162,7 @@ test("partial guardian replacement remains fenced through coordinator reconstruc
160
162
  replacementPromise = replacement.replaceIncompatibleOwner()
161
163
  void replacementPromise.catch(() => {})
162
164
  await restoreStarted.promise
163
- assert.equal(JSON.parse(await fs.readFile(statePath, "utf8")).recovery.guardian.socketPath, partialSocketPath)
165
+ expect(JSON.parse(await fs.readFile(statePath, "utf8")).recovery.guardian.socketPath).toBe(partialSocketPath)
164
166
 
165
167
  let mutationError
166
168
 
@@ -186,12 +188,12 @@ test("partial guardian replacement remains fenced through coordinator reconstruc
186
188
  contender.disconnect()
187
189
  contender = undefined
188
190
  continueRestore.resolve(undefined)
189
- await assert.rejects(replacementPromise, /injected reconstruction stop after fence audit/)
191
+ await expect(replacementPromise).rejects.toThrow(/injected reconstruction stop after fence audit/)
190
192
  replacementPromise = undefined
191
- assert.match(mutationError instanceof Error ? mutationError.message : "", /fenced while an owner replacement is prepared/)
192
- assert.match(contenderError instanceof Error ? contenderError.message : "", /another owner replacement candidate is already prepared/i)
193
- assert.notEqual(JSON.parse(await fs.readFile(statePath, "utf8")).recovery.guardian.socketPath, `${statePath}.split3-guardian.sock`)
194
- assert.equal((await sendControlCommand({command: {command: "status"}, path: socketPath})).daemonPid, owner.pid)
193
+ expect(mutationError instanceof Error ? mutationError.message : "").toMatch(/fenced while an owner replacement is prepared/)
194
+ expect(contenderError instanceof Error ? contenderError.message : "").toMatch(/another owner replacement candidate is already prepared/i)
195
+ expect(JSON.parse(await fs.readFile(statePath, "utf8")).recovery.guardian.socketPath).not.toBe(`${statePath}.split3-guardian.sock`)
196
+ expect((await sendControlCommand({command: {command: "status"}, path: socketPath})).daemonPid).toBe(owner.pid)
195
197
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: socketPath})
196
198
 
197
199
  await fs.writeFile(path.join(releasePath, "worker.fifo"), "drained\n")
@@ -243,7 +245,7 @@ test("partial guardian replacement persists the coordinator only after ownership
243
245
  await writeConfig(configPath, config({controlPath: socketPath, extraCompanion: false, statePath}))
244
246
  owner = spawn(process.execPath, [binPath, "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
245
247
  await waitForLog(owner, "control socket listening")
246
- assert.ok(owner.pid)
248
+ if (!owner.pid) throw new Error("Missing required fixture: owner.pid")
247
249
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath, revision: "v1"}, path: socketPath})
248
250
  const state = JSON.parse(await fs.readFile(statePath, "utf8"))
249
251
 
@@ -281,11 +283,8 @@ test("partial guardian replacement persists the coordinator only after ownership
281
283
  replacementPromise = replacement.replaceIncompatibleOwner()
282
284
  void replacementPromise.catch(() => {})
283
285
  await ownershipConfirmationStarted.promise
284
- assert.notEqual(
285
- JSON.parse(await fs.readFile(statePath, "utf8")).recovery.guardian.socketPath,
286
- `${statePath}.split3-guardian.sock`,
287
- "durable state must not name the coordinator before its legacy ownership claim is confirmed"
288
- )
286
+ // Durable state must not name the coordinator before its legacy ownership claim is confirmed.
287
+ expect(JSON.parse(await fs.readFile(statePath, "utf8")).recovery.guardian.socketPath).not.toBe(`${statePath}.split3-guardian.sock`)
289
288
  continueOwnershipConfirmation.resolve(undefined)
290
289
  await replacementPromise
291
290
  replacementPromise = undefined
@@ -325,7 +324,7 @@ test("partial guardian replacement persists the coordinator only after ownership
325
324
  }
326
325
  })
327
326
 
328
- test("failed partial upgrade resumes an incumbent retired release drain", {timeout: 5000}, async () => {
327
+ test("failed partial upgrade resumes an incumbent retired release drain", {timeoutMs: 5000}, async () => {
329
328
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-owner-replacement-partial-drain-"))
330
329
  const socketPath = path.join(root, "rollbridge.sock")
331
330
  const statePath = path.join(root, "state.json")
@@ -345,7 +344,7 @@ test("failed partial upgrade resumes an incumbent retired release drain", {timeo
345
344
  await writeConfig(configPath, config({controlPath: socketPath, extraCompanion: false, statePath}))
346
345
  owner = spawn(process.execPath, [binPath, "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
347
346
  await waitForLog(owner, "control socket listening")
348
- assert.ok(owner.pid)
347
+ if (!owner.pid) throw new Error("Missing required fixture: owner.pid")
349
348
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath: v1Path, revision: "v1"}, path: socketPath})
350
349
  const v1Status = await sendControlCommand({command: {command: "status"}, path: socketPath})
351
350
  const proxyPort = /** @type {{port?: number}} */ (v1Status.proxy).port
@@ -355,7 +354,7 @@ test("failed partial upgrade resumes an incumbent retired release drain", {timeo
355
354
  await sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: socketPath})
356
355
  const draining = await sendControlCommand({command: {command: "status"}, path: socketPath})
357
356
 
358
- assert.equal(releaseState(draining, "v1"), "draining")
357
+ expect(releaseState(draining, "v1")).toBe("draining")
359
358
  const state = JSON.parse(await fs.readFile(statePath, "utf8"))
360
359
 
361
360
  backendGuardianIdentity = {...state.recovery.guardian}
@@ -375,15 +374,15 @@ test("failed partial upgrade resumes an incumbent retired release drain", {timeo
375
374
  })
376
375
 
377
376
  candidate = replacement
378
- await assert.rejects(() => replacement.replaceIncompatibleOwner(), /provenance mismatch/)
379
- assert.equal(owner.exitCode, null)
380
- assert.equal(owner.signalCode, null)
377
+ await expect(replacement.replaceIncompatibleOwner()).rejects.toThrow(/provenance mismatch/)
378
+ expect(owner.exitCode).toBe(null)
379
+ expect(owner.signalCode).toBe(null)
381
380
  const releaseDrained = waitForLog(owner, "release drained")
382
381
 
383
382
  retainedConnection.destroy()
384
383
  await fs.writeFile(path.join(v1Path, "worker.fifo"), "drained\n")
385
384
  await releaseDrained
386
- assert.equal(releaseState(await sendControlCommand({command: {command: "status"}, path: socketPath}), "v1"), "stopped")
385
+ expect(releaseState(await sendControlCommand({command: {command: "status"}, path: socketPath}), "v1")).toBe("stopped")
387
386
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: socketPath})
388
387
 
389
388
  await fs.writeFile(path.join(v2Path, "worker.fifo"), "drained\n")
@@ -407,9 +406,7 @@ test("failed partial upgrade resumes an incumbent retired release drain", {timeo
407
406
  }
408
407
  })
409
408
 
410
- test("partial guardian classification failures preserve the incumbent, children, and retained stream", async (t) => {
411
- for (const fault of ["malformed-capability", "wrong-pid", "wrong-provenance"]) {
412
- await t.test(fault, async () => {
409
+ test.each(["malformed-capability", "wrong-pid", "wrong-provenance"])("partial guardian classification failure %s preserves the incumbent, children, and retained stream", async fault => {
413
410
  const root = await fs.mkdtemp(path.join(os.tmpdir(), `rollbridge-owner-replacement-partial-${fault}-`))
414
411
  const socketPath = path.join(root, "rollbridge.sock")
415
412
  const statePath = path.join(root, "state.json")
@@ -427,7 +424,7 @@ test("partial guardian classification failures preserve the incumbent, children,
427
424
  await writeConfig(configPath, config({controlPath: socketPath, extraCompanion: false, statePath}))
428
425
  owner = spawn(process.execPath, [binPath, "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
429
426
  await waitForLog(owner, "control socket listening")
430
- assert.ok(owner.pid)
427
+ if (!owner.pid) throw new Error("Missing required fixture: owner.pid")
431
428
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath, revision: "v1"}, path: socketPath})
432
429
  const before = await sendControlCommand({command: {command: "status"}, path: socketPath})
433
430
  const processPids = [releaseProcessPid(before, "v1", "worker"), releaseProcessPid(before, "v1", "web")]
@@ -465,14 +462,14 @@ test("partial guardian classification failures preserve the incumbent, children,
465
462
  ? /does not own socket|does not match the retained guardian command and socket/
466
463
  : /provenance mismatch/
467
464
 
468
- await assert.rejects(() => replacement.replaceIncompatibleOwner(), expected)
469
- assert.equal(owner.exitCode, null)
470
- assert.equal(owner.signalCode, null)
471
- assert.equal(retainedClosed, false)
472
- assert.equal(retainedConnection.destroyed, false)
473
- for (const pid of processPids) assert.doesNotThrow(() => process.kill(pid, 0))
474
- assert.equal(releaseProcessPid(await sendControlCommand({command: {command: "status"}, path: socketPath}), "v1", "worker"), processPids[0])
475
- await assert.rejects(fs.access(`${statePath}.split3-guardian.sock`), {code: "ENOENT"})
465
+ await expect(replacement.replaceIncompatibleOwner()).rejects.toThrow(expected)
466
+ expect(owner.exitCode).toBe(null)
467
+ expect(owner.signalCode).toBe(null)
468
+ expect(retainedClosed).toBe(false)
469
+ expect(retainedConnection.destroyed).toBe(false)
470
+ for (const pid of processPids) await expect(() => process.kill(pid, 0)).not.toThrow()
471
+ expect(releaseProcessPid(await sendControlCommand({command: {command: "status"}, path: socketPath}), "v1", "worker")).toBe(processPids[0])
472
+ await expect(fs.access(`${statePath}.split3-guardian.sock`)).rejects.toMatchObject({code: "ENOENT"})
476
473
 
477
474
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: socketPath})
478
475
 
@@ -487,8 +484,6 @@ test("partial guardian classification failures preserve the incumbent, children,
487
484
  await stopGuardian(statePath)
488
485
  await fs.rm(root, {force: true, recursive: true})
489
486
  }
490
- })
491
- }
492
487
  })
493
488
 
494
489
  test("first pre-split package upgrade is explicitly disruptive and later replacements are atomic", async () => {
@@ -515,7 +510,7 @@ test("first pre-split package upgrade is explicitly disruptive and later replace
515
510
  await writeConfig(configPath, config({controlPath: socketPath, extraCompanion: false, proxyPort, statePath}))
516
511
  owner = spawn(process.execPath, [legacyDaemonPath, "daemon", "--config", path.basename(configPath)], {cwd: root, stdio: ["ignore", "pipe", "pipe"]})
517
512
  await waitForLog(owner, "control socket listening")
518
- assert.ok(owner.pid)
513
+ if (!owner.pid) throw new Error("Missing required fixture: owner.pid")
519
514
  await fs.writeFile(daemonPidPath, `${owner.pid}\n`)
520
515
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath, revision: "v1"}, path: socketPath})
521
516
  const legacyStatus = await sendControlCommand({command: {command: "status"}, path: socketPath})
@@ -527,20 +522,22 @@ test("first pre-split package upgrade is explicitly disruptive and later replace
527
522
  await writeConfig(configPath, config({controlPath: socketPath, extraCompanion: true, proxyPort, statePath}))
528
523
  const mismatchedUpgrade = await runEnsureDaemon({configPath, daemonPidPath, logPath: path.join(root, "mismatch.log"), packagePath: firstPackagePath, runtimePath})
529
524
 
530
- assert.equal(mismatchedUpgrade.code, 1)
531
- assert.match(await fs.readFile(path.join(root, "mismatch.log"), "utf8"), /legacy guardian bridge requires the incumbent config identity unchanged/)
532
- assert.equal(interruptedConnection.destroyed, false, "config mismatch must leave the legacy listener serving")
533
- assert.equal(releaseProcessPid(await sendControlCommand({command: {command: "status"}, path: socketPath}), "v1", "worker"), legacyWorkerPid)
525
+ expect(mismatchedUpgrade.code).toBe(1)
526
+ expect(await fs.readFile(path.join(root, "mismatch.log"), "utf8")).toMatch(/legacy guardian bridge requires the incumbent config identity unchanged/)
527
+ // Config mismatch must leave the legacy listener serving.
528
+ expect(interruptedConnection.destroyed).toBe(false)
529
+ expect(releaseProcessPid(await sendControlCommand({command: {command: "status"}, path: socketPath}), "v1", "worker")).toBe(legacyWorkerPid)
534
530
  await writeConfig(configPath, config({controlPath: socketPath, extraCompanion: false, proxyPort, statePath}))
535
531
  blockedUpgradeGuardian = net.createServer()
536
532
 
537
533
  await listenUnix(blockedUpgradeGuardian, `${statePath}.split3-guardian.sock`)
538
534
  const blockedUpgrade = await runEnsureDaemon({configPath, daemonPidPath, logPath: path.join(root, "blocked.log"), packagePath: firstPackagePath, runtimePath})
539
535
 
540
- assert.equal(blockedUpgrade.code, 1)
541
- assert.match(await fs.readFile(path.join(root, "blocked.log"), "utf8"), /Legacy upgrade guardian socket .* already exists; refusing legacy upgrade/)
542
- assert.equal(interruptedConnection.destroyed, false, "candidate preparation failure must leave the legacy listener serving")
543
- assert.equal(releaseProcessPid(await sendControlCommand({command: {command: "status"}, path: socketPath}), "v1", "worker"), legacyWorkerPid)
536
+ expect(blockedUpgrade.code).toBe(1)
537
+ expect(await fs.readFile(path.join(root, "blocked.log"), "utf8")).toMatch(/Legacy upgrade guardian socket .* already exists; refusing legacy upgrade/)
538
+ // Candidate preparation failure must leave the legacy listener serving.
539
+ expect(interruptedConnection.destroyed).toBe(false)
540
+ expect(releaseProcessPid(await sendControlCommand({command: {command: "status"}, path: socketPath}), "v1", "worker")).toBe(legacyWorkerPid)
544
541
  await closeServer(blockedUpgradeGuardian)
545
542
  const firstUpgrade = await run(process.execPath, [
546
543
  path.join(firstPackagePath, "bin", "rollbridge"), "ensure-daemon", "--config", configPath,
@@ -548,8 +545,8 @@ test("first pre-split package upgrade is explicitly disruptive and later replace
548
545
  "--daemon-pid-path", daemonPidPath, "--daemon-start-timeout-ms", "3000"
549
546
  ])
550
547
 
551
- assert.equal(firstUpgrade.code, 0, `${firstUpgrade.stderr}\n${await fs.readFile(path.join(root, "first.log"), "utf8")}`)
552
- assert.deepEqual(JSON.parse(firstUpgrade.stdout).ownerTransition, {
548
+ expect({value: firstUpgrade.code, context: `${firstUpgrade.stderr}\n${await fs.readFile(path.join(root, "first.log"), "utf8")}`}).toMatchObject({value: 0})
549
+ expect(JSON.parse(firstUpgrade.stdout).ownerTransition).toEqual({
553
550
  disruptive: true,
554
551
  mode: "legacy-first-upgrade",
555
552
  reason: "retained guardian and daemon lacked atomic replacement protocol"
@@ -557,13 +554,13 @@ test("first pre-split package upgrade is explicitly disruptive and later replace
557
554
  await interrupted
558
555
  const bridged = await sendControlCommand({command: {command: "status"}, path: socketPath})
559
556
 
560
- assert.deepEqual(bridged.ownerTransition, {
557
+ expect(bridged.ownerTransition).toEqual({
561
558
  disruptive: true,
562
559
  mode: "legacy-first-upgrade",
563
560
  reason: "retained guardian and daemon lacked atomic replacement protocol"
564
561
  })
565
- assert.equal(releaseProcessPid(bridged, "v1", "worker"), legacyWorkerPid)
566
- assert.equal(bridged.activeReleaseId, "v1")
562
+ expect(releaseProcessPid(bridged, "v1", "worker")).toBe(legacyWorkerPid)
563
+ expect(bridged.activeReleaseId).toBe("v1")
567
564
 
568
565
  retainedConnection = await openWebSocket(proxyPort)
569
566
  let retainedConnectionClosed = false
@@ -576,13 +573,14 @@ test("first pre-split package upgrade is explicitly disruptive and later replace
576
573
  "--daemon-pid-path", daemonPidPath, "--daemon-start-timeout-ms", "3000"
577
574
  ])
578
575
 
579
- assert.equal(secondUpgrade.code, 0, `${secondUpgrade.stderr}\n${await fs.readFile(path.join(root, "second.log"), "utf8")}`)
576
+ expect({value: secondUpgrade.code, context: `${secondUpgrade.stderr}\n${await fs.readFile(path.join(root, "second.log"), "utf8")}`}).toMatchObject({value: 0})
580
577
  currentControlPath = nextSocketPath
581
- assert.equal(retainedConnectionClosed, false, "protocol-capable replacement must retain established proxy connections")
578
+ // Protocol-capable replacement must retain established proxy connections.
579
+ expect(retainedConnectionClosed).toBe(false)
582
580
  const replaced = await sendControlCommand({command: {command: "status"}, path: nextSocketPath})
583
581
 
584
- assert.equal(releaseProcessPid(replaced, "v1", "worker"), legacyWorkerPid)
585
- assert.equal(replaced.activeReleaseId, "v1")
582
+ expect(releaseProcessPid(replaced, "v1", "worker")).toBe(legacyWorkerPid)
583
+ expect(replaced.activeReleaseId).toBe("v1")
586
584
  retainedConnection.resetAndDestroy()
587
585
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: nextSocketPath})
588
586
  await fs.writeFile(path.join(releasePath, "worker.fifo"), "drained\n")
@@ -630,7 +628,7 @@ test("replacement-capable guardian without daemon recovery aborts before handoff
630
628
  await writeConfig(configPath, config({controlPath: socketPath, extraCompanion: false, statePath}))
631
629
  owner = spawn(process.execPath, [path.join(intermediatePackagePath, "bin", "rollbridge"), "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
632
630
  await waitForLog(owner, "control socket listening")
633
- assert.ok(owner.pid)
631
+ if (!owner.pid) throw new Error("Missing required fixture: owner.pid")
634
632
  await fs.writeFile(daemonPidPath, `${owner.pid}\n`)
635
633
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath: v1Path, revision: "v1"}, path: socketPath})
636
634
  const active = await sendControlCommand({command: {command: "status"}, path: socketPath})
@@ -641,13 +639,14 @@ test("replacement-capable guardian without daemon recovery aborts before handoff
641
639
  const workerPid = releaseProcessPid(before, "v1", "worker")
642
640
  const replacement = await runEnsureDaemon({configPath, daemonPidPath, logPath: daemonLogPath, packagePath: repoRoot, runtimePath})
643
641
 
644
- assert.equal(replacement.code, 1)
645
- assert.match(await fs.readFile(daemonLogPath, "utf8"), /persistent Rollbridge guardian predates daemon recovery/)
642
+ expect(replacement.code).toBe(1)
643
+ expect(await fs.readFile(daemonLogPath, "utf8")).toMatch(/persistent Rollbridge guardian predates daemon recovery/)
646
644
  await waitForFile(abortedPath)
647
645
  const preserved = await sendControlCommand({command: {command: "status"}, path: socketPath})
648
646
 
649
- assert.equal(preserved.daemonPid, before.daemonPid, "capability rejection must leave the incumbent daemon serving")
650
- assert.equal(releaseProcessPid(preserved, "v1", "worker"), workerPid)
647
+ // Capability rejection must leave the incumbent daemon serving.
648
+ expect(preserved.daemonPid).toBe(before.daemonPid)
649
+ expect(releaseProcessPid(preserved, "v1", "worker")).toBe(workerPid)
651
650
  retainedConnection.destroy()
652
651
  retainedConnection = undefined
653
652
  await fs.writeFile(path.join(v1Path, "worker.fifo"), "drained\n")
@@ -685,17 +684,18 @@ test("ensure-daemon owns and reports the exact candidate exit before readiness",
685
684
  ])
686
685
  candidate = JSON.parse(await fs.readFile(evidencePath, "utf8"))
687
686
 
688
- assert.equal(candidate.ppid, ensured.pid, "the recorded process must be the candidate spawned by this exact ensuring CLI")
689
- assert.notEqual(candidate.pid, ensured.pid)
690
- assert.deepEqual(candidate.argv.slice(2), [
687
+ // The recorded process must be the candidate spawned by this exact ensuring CLI.
688
+ expect(candidate.ppid).toBe(ensured.pid)
689
+ expect(candidate.pid).not.toBe(ensured.pid)
690
+ expect(candidate.argv.slice(2)).toEqual([
691
691
  "daemon", "--config", configPath,
692
692
  "--guardian-daemon-log-path", path.join(root, "daemon.log"),
693
693
  "--guardian-daemon-pid-path", path.join(root, "daemon.pid"),
694
694
  "--guardian-daemon-start-timeout-ms", "3000"
695
695
  ])
696
- assert.equal(ensured.code, 1)
697
- assert.match(ensured.stderr, new RegExp(`Rollbridge daemon candidate ${candidate.pid} exited before readiness \\(code 47, signal none\\)`))
698
- assert.doesNotMatch(ensured.stderr, /did not become ready within/)
696
+ expect(ensured.code).toBe(1)
697
+ expect(ensured.stderr).toMatch(new RegExp(`Rollbridge daemon candidate ${candidate.pid} exited before readiness \\(code 47, signal none\\)`))
698
+ expect(ensured.stderr).not.toMatch(/did not become ready within/)
699
699
  await waitForProcessExit(candidate.descendantPid)
700
700
  } finally {
701
701
  if (candidate?.descendantPid) {
@@ -706,8 +706,8 @@ test("ensure-daemon owns and reports the exact candidate exit before readiness",
706
706
  })
707
707
 
708
708
  test("legacy disruptive bridge rejects non-protocol guardian failures exactly", () => {
709
- assert.equal(isLegacyGuardianPrepareDiagnostic("Guardian prepare-owner-replacement requires a process key"), true)
710
- assert.equal(isLegacyGuardianPrepareDiagnostic("Unknown guardian command: prepare-owner-replacement"), true)
709
+ expect(isLegacyGuardianPrepareDiagnostic("Guardian prepare-owner-replacement requires a process key")).toBe(true)
710
+ expect(isLegacyGuardianPrepareDiagnostic("Unknown guardian command: prepare-owner-replacement")).toBe(true)
711
711
  for (const diagnostic of [
712
712
  "Unknown guardian command: deploy",
713
713
  "Unknown guardian command: prepare-owner-replacement ",
@@ -716,7 +716,7 @@ test("legacy disruptive bridge rejects non-protocol guardian failures exactly",
716
716
  "Process guardian connection closed while awaiting prepare-owner-replacement",
717
717
  "Guardian owner authority mismatch",
718
718
  "Malformed guardian response"
719
- ]) assert.equal(isLegacyGuardianPrepareDiagnostic(diagnostic), false, diagnostic)
719
+ ]) expect({value: isLegacyGuardianPrepareDiagnostic(diagnostic), context: diagnostic}).toMatchObject({value: false})
720
720
  })
721
721
 
722
722
  test("ensure-daemon atomically replaces incompatible config, socket, and package authority", async () => {
@@ -753,26 +753,26 @@ test("ensure-daemon atomically replaces incompatible config, socket, and package
753
753
 
754
754
  const daemonLog = await fs.readFile(path.join(root, "daemon.log"), "utf8")
755
755
 
756
- assert.equal(ensured.code, 0, `${ensured.stderr}\n${daemonLog}`)
756
+ expect({value: ensured.code, context: `${ensured.stderr}\n${daemonLog}`}).toMatchObject({value: 0})
757
757
  const transferred = await sendControlCommand({command: {command: "status"}, path: newSocketPath})
758
758
  const newRuntime = /** @type {{digest: string, path: string}} */ (transferred.daemonRuntime)
759
759
 
760
- assert.equal(transferred.activeReleaseId, "v1")
761
- assert.deepEqual(transferred.releaseReferences, [{releaseId: "v1", releasePath: v1Path}])
762
- assert.equal(releaseProcessPid(transferred, "v1", "worker"), workerPid)
763
- assert.notEqual(newRuntime.digest, oldRuntime.digest)
764
- assert.equal(path.dirname(newRuntime.path), runtimePath)
760
+ expect(transferred.activeReleaseId).toBe("v1")
761
+ expect(transferred.releaseReferences).toEqual([{releaseId: "v1", releasePath: v1Path}])
762
+ expect(releaseProcessPid(transferred, "v1", "worker")).toBe(workerPid)
763
+ expect(newRuntime.digest).not.toBe(oldRuntime.digest)
764
+ expect(path.dirname(newRuntime.path)).toBe(runtimePath)
765
765
 
766
766
  await fs.rm(packagePath, {force: true, recursive: true})
767
767
  await sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: newSocketPath})
768
768
  const deployed = await sendControlCommand({command: {command: "status"}, path: newSocketPath})
769
769
 
770
- assert.equal(deployed.activeReleaseId, "v2")
771
- assert.deepEqual(deployed.releaseReferences, [
770
+ expect(deployed.activeReleaseId).toBe("v2")
771
+ expect(deployed.releaseReferences).toEqual([
772
772
  {releaseId: "v1", releasePath: v1Path},
773
773
  {releaseId: "v2", releasePath: v2Path}
774
774
  ])
775
- assert.equal(releaseProcessPid(deployed, "v1", "worker"), workerPid)
775
+ expect(releaseProcessPid(deployed, "v1", "worker")).toBe(workerPid)
776
776
 
777
777
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: newSocketPath})
778
778
  await Promise.all([
@@ -816,12 +816,12 @@ test("cross-version replacement fails closed without dropping a retained WebSock
816
816
  const processState = retiredReleases[0]?.processes.map(({id, pid, state}) => ({id, pid, state}))
817
817
  const proxyPort = /** @type {{port?: number}} */ (retired.proxy).port
818
818
 
819
- assert.deepEqual(processState?.map(({state}) => state), ["running", "running"])
819
+ expect(processState?.map(({state}) => state)).toEqual(["running", "running"])
820
820
  if (typeof proxyPort !== "number") throw new Error("Retained owner proxy is missing its port")
821
821
  retainedConnection = await openWebSocket(proxyPort)
822
822
  retainedConnection.once("close", () => { retainedConnectionClosed = true })
823
823
  await fs.rm(socketPath)
824
- await assert.rejects(fs.access(socketPath), {code: "ENOENT"})
824
+ await expect(fs.access(socketPath)).rejects.toMatchObject({code: "ENOENT"})
825
825
  const state = JSON.parse(await fs.readFile(statePath, "utf8"))
826
826
  const guardianSocketPath = state.recovery.guardian.socketPath
827
827
  const expectedProcessKey = "release:v1:worker"
@@ -875,7 +875,7 @@ test("cross-version replacement fails closed without dropping a retained WebSock
875
875
 
876
876
  const incumbentPid = owner.pid
877
877
 
878
- assert.ok(incumbentPid)
878
+ if (!incumbentPid) throw new Error("Missing required fixture: incumbentPid")
879
879
  const candidate = new RollbridgeDaemon({
880
880
  config: normalizeConfig(config({controlPath: socketPath, extraCompanion: false, statePath})),
881
881
  configPath,
@@ -883,27 +883,26 @@ test("cross-version replacement fails closed without dropping a retained WebSock
883
883
  logger: () => {}
884
884
  })
885
885
  replacement = candidate
886
- await assert.rejects(
887
- () => candidate.replaceIncompatibleOwner(),
888
- /cannot safely complete atomic owner replacement through the older retained guardian while the incumbent control socket is absent; incumbent owner and connections were preserved/i
889
- )
890
- assert.equal(committedProcessKey, expectedProcessKey)
891
- assert.equal(owner.exitCode, null)
892
- assert.equal(owner.signalCode, null)
893
- assert.doesNotThrow(() => process.kill(incumbentPid, 0))
894
- assert.equal(retainedConnectionClosed, false, "failed compatibility handoff must leave retained connections serving")
895
- assert.equal(retainedConnection.destroyed, false, "failed compatibility handoff must preserve the incumbent listener")
886
+ await expect(candidate.replaceIncompatibleOwner()).rejects.toThrow(/cannot safely complete atomic owner replacement through the older retained guardian while the incumbent control socket is absent; incumbent owner and connections were preserved/i)
887
+ expect(committedProcessKey).toBe(expectedProcessKey)
888
+ expect(owner.exitCode).toBe(null)
889
+ expect(owner.signalCode).toBe(null)
890
+ await expect(() => process.kill(incumbentPid, 0)).not.toThrow()
891
+ // Failed compatibility handoff must leave retained connections serving.
892
+ expect(retainedConnectionClosed).toBe(false)
893
+ // Failed compatibility handoff must preserve the incumbent listener.
894
+ expect(retainedConnection.destroyed).toBe(false)
896
895
  for (const {pid} of processState || []) {
897
896
  if (typeof pid !== "number") throw new Error("Retained process is missing its PID")
898
- assert.doesNotThrow(() => process.kill(pid, 0))
897
+ await expect(() => process.kill(pid, 0)).not.toThrow()
899
898
  }
900
899
  transactionAudit = new GuardianClient(state.recovery.guardian)
901
900
  await transactionAudit.connect()
902
901
  const transactionStatus = /** @type {{committedReplacementId: string | null, ownerClaimed: boolean, retirementPending?: boolean}} */ (await transactionAudit.replacementStatus())
903
902
 
904
- assert.equal(transactionStatus.committedReplacementId, null)
905
- assert.equal(transactionStatus.ownerClaimed, true)
906
- assert.equal(transactionStatus.retirementPending, false)
903
+ expect(transactionStatus.committedReplacementId).toBe(null)
904
+ expect(transactionStatus.ownerClaimed).toBe(true)
905
+ expect(transactionStatus.retirementPending).toBe(false)
907
906
  } finally {
908
907
  if (replacement?.controlCommandsReady) {
909
908
  await Promise.all([
@@ -964,8 +963,8 @@ test("cross-version replacement preserves committed-owner proof until commit the
964
963
  await owner.deploy({releaseId: "v1", releasePath, revision: "v1"})
965
964
  const ownerProcess = owner.guardian?.processes.values().next().value
966
965
 
967
- assert.ok(owner.guardian)
968
- assert.ok(ownerProcess)
966
+ if (!owner.guardian) throw new Error("Missing required fixture: owner.guardian")
967
+ if (!ownerProcess) throw new Error("Missing required fixture: ownerProcess")
969
968
  await owner.guardian.request({
970
969
  command: "register",
971
970
  definition: ownerProcess.definition,
@@ -977,7 +976,7 @@ test("cross-version replacement preserves committed-owner proof until commit the
977
976
  const expectedProcessKeys = new Set(running.releases[0]?.processes.map(({id}) => `release:v1:${id}`))
978
977
  const runningProxyPort = /** @type {{port?: number}} */ (running.proxy).port
979
978
 
980
- assert.deepEqual(processState?.map(({state}) => state), ["running", "running"])
979
+ expect(processState?.map(({state}) => state)).toEqual(["running", "running"])
981
980
  if (typeof runningProxyPort !== "number") throw new Error("Retained owner proxy is missing its port")
982
981
  retainedConnection = await openWebSocket(runningProxyPort)
983
982
  retainedConnection.once("close", () => { retainedConnectionClosed = true })
@@ -1032,8 +1031,8 @@ test("cross-version replacement preserves committed-owner proof until commit the
1032
1031
  state.recovery.guardian.socketPath = compatibilitySocketPath
1033
1032
  await fs.writeFile(statePath, `${JSON.stringify(state)}\n`)
1034
1033
 
1035
- assert.equal((await owner.guardian?.replacementStatus())?.ownerClaimed, true)
1036
- await assert.rejects(fs.access(socketPath), {code: "ENOENT"})
1034
+ expect((await owner.guardian?.replacementStatus())?.ownerClaimed).toBe(true)
1035
+ await expect(fs.access(socketPath)).rejects.toMatchObject({code: "ENOENT"})
1037
1036
 
1038
1037
  replacement = new RollbridgeDaemon({
1039
1038
  config: daemonConfig,
@@ -1059,21 +1058,26 @@ test("cross-version replacement preserves committed-owner proof until commit the
1059
1058
  const recovered = await sendControlCommand({command: {command: "status"}, path: socketPath})
1060
1059
  const recoveredReleases = /** @type {{connectionCount: number, connections: {http: number, websocket: number}, processes: {id: string, pid?: number, state: string}[], releaseId: string, state: string}[]} */ (recovered.releases)
1061
1060
 
1062
- assert.equal(owner.ownerRetired, true, "committed incumbent must observe retirement without a public control socket")
1063
- assert.equal(owner.proxyServer?.listening, false, "committed incumbent must stop accepting stale proxy traffic")
1064
- assert.equal(recovered.activeReleaseId, "v1")
1065
- assert.equal(committedProcessKey, committedOwnerProcessKey)
1066
- assert.equal(recoveredKeysAtCommit?.has(committedOwnerProcessKey), false)
1067
- assert.deepEqual(candidateRecoveredKeys, expectedProcessKeys)
1068
- assert.equal([...replacement.guardian?.processes.keys() || []][0], candidateProcessKey)
1069
- assert.equal(retainedConnectionClosed, false, "successful compatibility handoff must preserve retained connections")
1070
- assert.equal(retainedConnection.destroyed, false, "successful compatibility handoff must leave the retained listener serving")
1071
- assert.deepEqual(recoveredReleases[0]?.connections, {http: 0, websocket: 1}, "candidate must inherit exact live incumbent connection counts")
1072
- assert.deepEqual(recovered.releaseReferences, [{releaseId: "v1", releasePath}])
1073
- assert.deepEqual(recoveredReleases[0]?.processes.map(({id, pid, state}) => ({id, pid, state})), processState)
1061
+ // Committed incumbent must observe retirement without a public control socket.
1062
+ expect(owner.ownerRetired).toBe(true)
1063
+ // Committed incumbent must stop accepting stale proxy traffic.
1064
+ expect(owner.proxyServer?.listening).toBe(false)
1065
+ expect(recovered.activeReleaseId).toBe("v1")
1066
+ expect(committedProcessKey).toBe(committedOwnerProcessKey)
1067
+ expect(recoveredKeysAtCommit?.has(committedOwnerProcessKey)).toBe(false)
1068
+ expect(candidateRecoveredKeys).toEqual(expectedProcessKeys)
1069
+ expect([...replacement.guardian?.processes.keys() || []][0]).toBe(candidateProcessKey)
1070
+ // Successful compatibility handoff must preserve retained connections.
1071
+ expect(retainedConnectionClosed).toBe(false)
1072
+ // Successful compatibility handoff must leave the retained listener serving.
1073
+ expect(retainedConnection.destroyed).toBe(false)
1074
+ // Candidate must inherit exact live incumbent connection counts.
1075
+ expect(recoveredReleases[0]?.connections).toEqual({http: 0, websocket: 1})
1076
+ expect(recovered.releaseReferences).toEqual([{releaseId: "v1", releasePath}])
1077
+ expect(recoveredReleases[0]?.processes.map(({id, pid, state}) => ({id, pid, state}))).toEqual(processState)
1074
1078
  for (const {pid} of processState || []) {
1075
1079
  if (typeof pid !== "number") throw new Error("Retained process is missing its PID")
1076
- assert.doesNotThrow(() => process.kill(pid, 0))
1080
+ await expect(() => process.kill(pid, 0)).not.toThrow()
1077
1081
  }
1078
1082
  secondRetainedConnection = await openWebSocket(runningProxyPort)
1079
1083
  intermediate = replacement
@@ -1087,18 +1091,21 @@ test("cross-version replacement preserves committed-owner proof until commit the
1087
1091
  await replacement.replaceIncompatibleOwner()
1088
1092
  const repeatedHandoff = /** @type {{connections: {http: number, websocket: number}, releaseId: string}[]} */ ((await sendControlCommand({command: {command: "status"}, path: socketPath})).releases)
1089
1093
 
1090
- assert.deepEqual(repeatedHandoff[0]?.connections, {http: 0, websocket: 2}, "successive control-less owners must aggregate each physical listener source")
1094
+ // Successive control-less owners must aggregate each physical listener source.
1095
+ expect(repeatedHandoff[0]?.connections).toEqual({http: 0, websocket: 2})
1091
1096
  await replacement.deploy({releaseId: "v2", releasePath: nextReleasePath, revision: "v2"})
1092
1097
  const draining = /** @type {{connectionCount: number, releaseId: string, state: string}[]} */ ((await sendControlCommand({command: {command: "status"}, path: socketPath})).releases)
1093
1098
  const retainedRelease = draining.find(({releaseId}) => releaseId === "v1")
1094
1099
 
1095
- assert.equal(retainedRelease?.state, "draining")
1096
- assert.equal(retainedRelease?.connectionCount, 2, "candidate must not stop the retained upstream while retired WebSockets are live")
1100
+ expect(retainedRelease?.state).toBe("draining")
1101
+ // Candidate must not stop the retained upstream while retired WebSockets are live.
1102
+ expect(retainedRelease?.connectionCount).toBe(2)
1097
1103
  // Send a masked empty WebSocket close frame so both retained proxy legs drain.
1098
1104
  retainedConnection.write(Buffer.from([0x88, 0x80, 0, 0, 0, 0]))
1099
1105
  if (owner.proxyClosePromise) await owner.proxyClosePromise
1100
1106
  if (!retainedConnectionClosed) await once(retainedConnection, "close")
1101
- assert.equal(retainedConnectionClosed, true, "retired incumbent must finish after its retained connections drain")
1107
+ // Retired incumbent must finish after its retained connections drain.
1108
+ expect(retainedConnectionClosed).toBe(true)
1102
1109
  const oneSourceDeadline = Date.now() + 3000
1103
1110
  let oneRetained
1104
1111
 
@@ -1109,8 +1116,9 @@ test("cross-version replacement preserves committed-owner proof until commit the
1109
1116
  await new Promise((resolve) => setTimeout(resolve, 10))
1110
1117
  }
1111
1118
 
1112
- assert.equal(oneRetained?.connectionCount, 1, "one retired listener source must not clear another source's live connection")
1113
- assert.equal(oneRetained?.processes.find(({id}) => id === "web")?.state, "running")
1119
+ // One retired listener source must not clear another source's live connection.
1120
+ expect(oneRetained?.connectionCount).toBe(1)
1121
+ expect(oneRetained?.processes.find(({id}) => id === "web")?.state).toBe("running")
1114
1122
  secondRetainedConnection.write(Buffer.from([0x88, 0x80, 0, 0, 0, 0]))
1115
1123
  if (intermediate.proxyClosePromise) await intermediate.proxyClosePromise
1116
1124
  const stopDeadline = Date.now() + 3000
@@ -1123,7 +1131,8 @@ test("cross-version replacement preserves committed-owner proof until commit the
1123
1131
  retainedWebStopped = !retained || retained.processes.find(({id}) => id === "web")?.state === "stopped"
1124
1132
  if (!retainedWebStopped) await new Promise((resolve) => setTimeout(resolve, 25))
1125
1133
  }
1126
- assert.equal(retainedWebStopped, true, "retained upstream must stop after the incumbent reports its final connection drain")
1134
+ // Retained upstream must stop after the incumbent reports its final connection drain.
1135
+ expect(retainedWebStopped).toBe(true)
1127
1136
  } finally {
1128
1137
  if (retainedGuardianSocketPath) {
1129
1138
  const cleanupState = JSON.parse(await fs.readFile(statePath, "utf8"))
@@ -1182,9 +1191,9 @@ test("replacement refuses to overwrite an unrelated live final control socket an
1182
1191
  "--daemon-pid-path", path.join(root, "daemon.pid"), "--daemon-start-timeout-ms", "1500"
1183
1192
  ])
1184
1193
 
1185
- assert.notEqual(replacement.code, 0)
1186
- assert.match(await fs.readFile(path.join(root, "daemon.log"), "utf8"), /final control socket.*already answers another live process/)
1187
- assert.equal((await sendControlCommand({command: {command: "status"}, path: oldSocketPath})).activeReleaseId, "v1")
1194
+ expect(replacement.code).not.toBe(0)
1195
+ expect(await fs.readFile(path.join(root, "daemon.log"), "utf8")).toMatch(/final control socket.*already answers another live process/)
1196
+ expect((await sendControlCommand({command: {command: "status"}, path: oldSocketPath})).activeReleaseId).toBe("v1")
1188
1197
 
1189
1198
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: oldSocketPath})
1190
1199
  await fs.writeFile(path.join(root, "v1", "worker.fifo"), "drained\n")
@@ -1229,8 +1238,8 @@ test("a committed replacement crash converges from stale public state", async ()
1229
1238
  await waitForLog(recovered, "control socket listening")
1230
1239
  const status = await sendControlCommand({command: {command: "status"}, path: newSocketPath})
1231
1240
 
1232
- assert.equal(status.activeReleaseId, "v1")
1233
- assert.deepEqual(status.releaseReferences, [{releaseId: "v1", releasePath}])
1241
+ expect(status.activeReleaseId).toBe("v1")
1242
+ expect(status.releaseReferences).toEqual([{releaseId: "v1", releasePath}])
1234
1243
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: newSocketPath})
1235
1244
  await fs.writeFile(path.join(releasePath, "worker.fifo"), "drained\n")
1236
1245
  await shutdown
@@ -1266,11 +1275,11 @@ test("replacement transfers an unchanged fixed proxy listener without reusePort"
1266
1275
  candidate = spawn(process.execPath, [binPath, "daemon", "--config", configPath, "--replace-owner"], {stdio: ["ignore", "pipe", "pipe"]})
1267
1276
  const output = await collectUntilExitOrLog(candidate, "owner replacement committed")
1268
1277
 
1269
- assert.equal(output.message, "owner replacement committed", output.output)
1278
+ expect({value: output.message, context: output.output}).toMatchObject({value: "owner replacement committed"})
1270
1279
  const status = await sendControlCommand({command: {command: "status"}, path: newSocketPath})
1271
1280
  const proxy = /** @type {{port: number}} */ (status.proxy)
1272
1281
 
1273
- assert.equal(proxy.port, proxyPort)
1282
+ expect(proxy.port).toBe(proxyPort)
1274
1283
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: newSocketPath})
1275
1284
  await fs.writeFile(path.join(releasePath, "worker.fifo"), "drained\n")
1276
1285
  await shutdown
@@ -1301,7 +1310,7 @@ test("direct retired-listener drain updates reach a recovered committed owner",
1301
1310
  retainedConnection = await openWebSocket(proxyPort)
1302
1311
  candidate = new RollbridgeDaemon({config: daemonConfig, logger: () => {}})
1303
1312
  await candidate.replaceIncompatibleOwner()
1304
- assert.equal(candidate.status().releases[0]?.connectionCount, 1)
1313
+ expect(candidate.status().releases[0]?.connectionCount).toBe(1)
1305
1314
 
1306
1315
  candidate.incumbentListenerControl?.close()
1307
1316
  await candidate.closeServer(candidate.controlServer)
@@ -1309,7 +1318,7 @@ test("direct retired-listener drain updates reach a recovered committed owner",
1309
1318
  candidate.guardian?.disconnect()
1310
1319
  recovered = new RollbridgeDaemon({config: daemonConfig, logger: () => {}})
1311
1320
  await recovered.start({exposeControl: false})
1312
- assert.equal(recovered.status().releases[0]?.connectionCount, 1)
1321
+ expect(recovered.status().releases[0]?.connectionCount).toBe(1)
1313
1322
 
1314
1323
  retainedConnection.write(Buffer.from([0x88, 0x80, 0, 0, 0, 0]))
1315
1324
  if (owner.proxyClosePromise) await owner.proxyClosePromise
@@ -1318,7 +1327,7 @@ test("direct retired-listener drain updates reach a recovered committed owner",
1318
1327
  while (Date.now() < deadline && recovered.status().releases[0]?.connectionCount !== 0) {
1319
1328
  await new Promise((resolve) => setTimeout(resolve, 10))
1320
1329
  }
1321
- assert.equal(recovered.status().releases[0]?.connectionCount, 0)
1330
+ expect(recovered.status().releases[0]?.connectionCount).toBe(0)
1322
1331
  } finally {
1323
1332
  retainedConnection?.destroy()
1324
1333
  await candidate?.closeServer(candidate.controlServer)
@@ -1337,7 +1346,7 @@ test("direct retired-listener drain updates reach a recovered committed owner",
1337
1346
  }
1338
1347
  })
1339
1348
 
1340
- test("direct listener publication failure rejects the committed replacement", {timeout: 5000}, async () => {
1349
+ test("direct listener publication failure rejects the committed replacement", {timeoutMs: 5000}, async () => {
1341
1350
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-owner-replacement-direct-listener-failure-"))
1342
1351
  const socketPath = path.join(root, "rollbridge.sock")
1343
1352
  const statePath = path.join(root, "state.json")
@@ -1355,10 +1364,7 @@ test("direct listener publication failure rejects the committed replacement", {t
1355
1364
  const replacement = new RollbridgeDaemon({config: daemonConfig, logger: () => {}})
1356
1365
 
1357
1366
  candidate = replacement
1358
- await assert.rejects(
1359
- () => replacement.replaceIncompatibleOwner(),
1360
- /Retired listener disconnected before publishing complete connection state/
1361
- )
1367
+ await expect(replacement.replaceIncompatibleOwner()).rejects.toThrow(/Retired listener disconnected before publishing complete connection state/)
1362
1368
  } finally {
1363
1369
  await candidate?.closeServer(candidate.controlServer)
1364
1370
  await candidate?.closeServer(candidate.proxyServer)
@@ -1393,7 +1399,7 @@ test("control-less fixed-proxy bind failure preserves incumbent authority", asyn
1393
1399
 
1394
1400
  candidate = replacement
1395
1401
  replacement.startProxy = async () => { throw new Error("injected fixed proxy bind failure") }
1396
- await assert.rejects(() => replacement.replaceIncompatibleOwner(), /injected fixed proxy bind failure/)
1402
+ await expect(replacement.replaceIncompatibleOwner()).rejects.toThrow(/injected fixed proxy bind failure/)
1397
1403
  const deadline = Date.now() + 3000
1398
1404
 
1399
1405
  while (Date.now() < deadline) {
@@ -1406,9 +1412,10 @@ test("control-less fixed-proxy bind failure preserves incumbent authority", asyn
1406
1412
  }
1407
1413
  }
1408
1414
 
1409
- assert.ok(resumedConnection, "incumbent proxy must resume after candidate bind failure")
1410
- assert.equal(owner.ownerRetired, false)
1411
- assert.deepEqual(await owner.guardian?.replacementStatus(), {
1415
+ // Incumbent proxy must resume after candidate bind failure.
1416
+ expect(resumedConnection).toBeTruthy()
1417
+ expect(owner.ownerRetired).toBe(false)
1418
+ expect(await owner.guardian?.replacementStatus()).toEqual({
1412
1419
  committedReplacementId: null,
1413
1420
  ownerClaimed: true,
1414
1421
  retirementFailed: false,
@@ -1458,15 +1465,15 @@ test("owner recovery finalizes a completed control-less listener handoff after c
1458
1465
  throw new Error("injected candidate exit before listener finalization")
1459
1466
  }
1460
1467
  }
1461
- await assert.rejects(() => replacement.replaceIncompatibleOwner(), /injected candidate exit before listener finalization/)
1468
+ await expect(replacement.replaceIncompatibleOwner()).rejects.toThrow(/injected candidate exit before listener finalization/)
1462
1469
  await replacement.closeServer(replacement.controlServer)
1463
1470
  await replacement.closeServer(replacement.proxyServer)
1464
1471
 
1465
1472
  recovered = new RollbridgeDaemon({config: daemonConfig, logger: () => {}})
1466
1473
  await recovered.start({exposeControl: false})
1467
1474
 
1468
- assert.equal(recovered.getProxyPort(), proxyPort)
1469
- assert.equal((await recovered.guardian?.replacementStatus())?.retirementPending, false)
1475
+ expect(recovered.getProxyPort()).toBe(proxyPort)
1476
+ expect((await recovered.guardian?.replacementStatus())?.retirementPending).toBe(false)
1470
1477
  } finally {
1471
1478
  await candidate?.closeServer(candidate.controlServer)
1472
1479
  await candidate?.closeServer(candidate.proxyServer)
@@ -1518,8 +1525,8 @@ test("control-less retirement clears an active source but omits a stopped releas
1518
1525
  await daemon.yieldControlLessOwnerListeners("replacement")
1519
1526
  await daemon.completeControlLessOwnerRetirement("replacement")
1520
1527
 
1521
- assert.deepEqual(published, [{releaseId: "active", sourceId: daemon.listenerSourceId}])
1522
- assert.equal(disconnected, true)
1528
+ expect(published).toEqual([{releaseId: "active", sourceId: daemon.listenerSourceId}])
1529
+ expect(disconnected).toBe(true)
1523
1530
  } finally {
1524
1531
  await fs.rm(root, {force: true, recursive: true})
1525
1532
  }
@@ -1543,7 +1550,7 @@ test("owner replacement preserves committed generation metadata without firing l
1543
1550
  owner = spawn(process.execPath, [binPath, "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
1544
1551
  await waitForLog(owner, "control socket listening")
1545
1552
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath, revision: "v1"}, path: oldSocketPath})
1546
- assert.equal(await fs.readFile(lifecycleLogPath, "utf8"), "activate:v1\n")
1553
+ expect(await fs.readFile(lifecycleLogPath, "utf8")).toBe("activate:v1\n")
1547
1554
 
1548
1555
  await writeConfig(configPath, config({activationLogPath: lifecycleLogPath, controlPath: newSocketPath, extraCompanion: true, statePath}))
1549
1556
  candidate = spawn(process.execPath, [binPath, "daemon", "--config", configPath, "--replace-owner"], {stdio: ["ignore", "pipe", "pipe"]})
@@ -1552,10 +1559,11 @@ test("owner replacement preserves committed generation metadata without firing l
1552
1559
  const status = await sendControlCommand({command: {command: "status"}, path: newSocketPath})
1553
1560
  const generationTransition = status.generationTransition
1554
1561
 
1555
- assert.equal(status.activeReleaseId, "v1")
1556
- assert.ok(generationTransition && typeof generationTransition === "object" && !Array.isArray(generationTransition))
1557
- assert.equal(generationTransition.phase, "committed")
1558
- assert.equal(await fs.readFile(lifecycleLogPath, "utf8"), "activate:v1\n", "owner replacement must not reactivate an already committed generation")
1562
+ expect(status.activeReleaseId).toBe("v1")
1563
+ if (!(generationTransition && typeof generationTransition === "object" && !Array.isArray(generationTransition))) throw new Error("Expected generation transition status")
1564
+ expect(generationTransition.phase).toBe("committed")
1565
+ // Owner replacement must not reactivate an already committed generation.
1566
+ expect(await fs.readFile(lifecycleLogPath, "utf8")).toBe("activate:v1\n")
1559
1567
 
1560
1568
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: newSocketPath})
1561
1569
  await fs.writeFile(path.join(releasePath, "worker.fifo"), "drained\n")
@@ -1594,23 +1602,24 @@ test("owner replacement excludes stopped retained releases from reserved process
1594
1602
  const activeWorkerPid = releaseProcessPid(before, "v2", "worker")
1595
1603
  const retained = /** @type {{releaseId: string, state: string}[]} */ (before.releases)
1596
1604
 
1597
- assert.equal(retained.find(({releaseId}) => releaseId === "v1")?.state, "stopped")
1605
+ expect(retained.find(({releaseId}) => releaseId === "v1")?.state).toBe("stopped")
1598
1606
  const persisted = JSON.parse(await fs.readFile(statePath, "utf8"))
1599
1607
  const guardian = new GuardianClient(persisted.recovery.guardian)
1600
1608
 
1601
1609
  await guardian.connect()
1602
- assert.ok((await guardian.inventory()).some(({key}) => key === "release:v1:worker"), "stopped release registration remains in authenticated guardian inventory")
1610
+ // Stopped release registration remains in authenticated guardian inventory.
1611
+ expect((await guardian.inventory()).some(({key}) => key === "release:v1:worker")).toBe(true)
1603
1612
  guardian.disconnect()
1604
1613
  await writeConfig(configPath, config({controlPath: newSocketPath, extraCompanion: true, statePath}))
1605
1614
  candidate = spawn(process.execPath, [binPath, "daemon", "--config", configPath, "--replace-owner"], {stdio: ["ignore", "pipe", "pipe"]})
1606
1615
  const output = await collectUntilExitOrLog(candidate, "owner replacement committed")
1607
1616
 
1608
- assert.equal(output.message, "owner replacement committed", output.output)
1617
+ expect({value: output.message, context: output.output}).toMatchObject({value: "owner replacement committed"})
1609
1618
  const recovered = await sendControlCommand({command: {command: "status"}, path: newSocketPath})
1610
1619
 
1611
- assert.equal(recovered.activeReleaseId, "v2")
1612
- assert.equal(releaseProcessPid(recovered, "v2", "worker"), activeWorkerPid)
1613
- assert.equal(/** @type {{releaseId: string}[]} */ (recovered.releases).some(({releaseId}) => releaseId === "v1"), false)
1620
+ expect(recovered.activeReleaseId).toBe("v2")
1621
+ expect(releaseProcessPid(recovered, "v2", "worker")).toBe(activeWorkerPid)
1622
+ expect(/** @type {{releaseId: string}[]} */ (recovered.releases).some(({releaseId}) => releaseId === "v1")).toBe(false)
1614
1623
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: newSocketPath})
1615
1624
 
1616
1625
  await fs.writeFile(path.join(v2Path, "worker.fifo"), "drained\n")
@@ -1622,7 +1631,7 @@ test("owner replacement excludes stopped retained releases from reserved process
1622
1631
  }
1623
1632
  })
1624
1633
 
1625
- test("pruned release connection completion closes the incumbent listener session", () => {
1634
+ test("pruned release connection completion closes the incumbent listener session", async () => {
1626
1635
  const daemon = new RollbridgeDaemon({
1627
1636
  config: normalizeConfig(config({controlPath: "/unused/control.sock", extraCompanion: false, statePath: "/unused/state.json"})),
1628
1637
  logger: () => {}
@@ -1637,16 +1646,13 @@ test("pruned release connection completion closes the incumbent listener session
1637
1646
  daemon.incumbentListenerControl = controlSession
1638
1647
  daemon.handleIncumbentListenerEvent({connections: {http: 0, websocket: 0}, event: "owner-connection-state", releaseId: "pruned"}, session)
1639
1648
 
1640
- assert.equal(closeCount, 1)
1641
- assert.equal(daemon.incumbentListenerControl, undefined)
1649
+ expect(closeCount).toBe(1)
1650
+ expect(daemon.incumbentListenerControl).toBe(undefined)
1642
1651
 
1643
1652
  daemon.incumbentListenerControl = controlSession
1644
- assert.throws(
1645
- () => daemon.handleIncumbentListenerEvent({connections: {http: 1, websocket: 0}, event: "owner-connection-state", releaseId: "pruned"}, session),
1646
- /unknown release pruned/
1647
- )
1648
- assert.equal(closeCount, 1)
1649
- assert.equal(daemon.incumbentListenerControl, session)
1653
+ await expect(() => daemon.handleIncumbentListenerEvent({connections: {http: 1, websocket: 0}, event: "owner-connection-state", releaseId: "pruned"}, session)).toThrow(/unknown release pruned/)
1654
+ expect(closeCount).toBe(1)
1655
+ expect(daemon.incumbentListenerControl).toBe(session)
1650
1656
  })
1651
1657
 
1652
1658
  test("same-authority owner replacement preserves completed activation compensation without replaying hooks", async () => {
@@ -1676,16 +1682,17 @@ test("same-authority owner replacement preserves completed activation compensati
1676
1682
  owner = spawn(process.execPath, [binPath, "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
1677
1683
  await waitForLog(owner, "control socket listening")
1678
1684
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath: v1Path, revision: "v1"}, path: oldSocketPath})
1679
- await assert.rejects(sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: oldSocketPath}), /activate command exited non-zero/)
1680
- assert.equal(await fs.readFile(lifecycleLogPath, "utf8"), "activate:v1\nretire:v1\nretire:v2\nactivate:v1\n")
1685
+ await expect(sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: oldSocketPath})).rejects.toThrow(/activate command exited non-zero/)
1686
+ expect(await fs.readFile(lifecycleLogPath, "utf8")).toBe("activate:v1\nretire:v1\nretire:v2\nactivate:v1\n")
1681
1687
 
1682
1688
  await writeConfig(configPath, failedConfig(oldSocketPath, false))
1683
1689
  candidate = spawn(process.execPath, [binPath, "daemon", "--config", configPath, "--replace-owner"], {stdio: ["ignore", "pipe", "pipe"]})
1684
1690
  await waitForLog(candidate, "owner replacement committed")
1685
1691
  const status = await sendControlCommand({command: {command: "status"}, path: oldSocketPath})
1686
- assert.equal(status.activeReleaseId, "v1")
1687
- assert.equal(status.generationTransition, undefined)
1688
- assert.equal(await fs.readFile(lifecycleLogPath, "utf8"), "activate:v1\nretire:v1\nretire:v2\nactivate:v1\n", "replacement must not replay completed compensation hooks")
1692
+ expect(status.activeReleaseId).toBe("v1")
1693
+ expect(status.generationTransition).toBe(undefined)
1694
+ // Replacement must not replay completed compensation hooks.
1695
+ expect(await fs.readFile(lifecycleLogPath, "utf8")).toBe("activate:v1\nretire:v1\nretire:v2\nactivate:v1\n")
1689
1696
 
1690
1697
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: oldSocketPath})
1691
1698
  await Promise.all([v1Path, v2Path].map((releasePath) => fs.writeFile(path.join(releasePath, "worker.fifo"), "drained\n")))
@@ -1725,18 +1732,18 @@ test("config-changing owner replacement proceeds after activation compensation c
1725
1732
  owner = spawn(process.execPath, [binPath, "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
1726
1733
  await waitForLog(owner, "control socket listening")
1727
1734
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath: v1Path, revision: "v1"}, path: oldSocketPath})
1728
- await assert.rejects(sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: oldSocketPath}), /activate command exited non-zero/)
1735
+ await expect(sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: oldSocketPath})).rejects.toThrow(/activate command exited non-zero/)
1729
1736
  await writeConfig(configPath, failedConfig(newSocketPath, true))
1730
1737
 
1731
1738
  candidate = spawn(process.execPath, [binPath, "daemon", "--config", configPath, "--replace-owner"], {stdio: ["ignore", "pipe", "pipe"]})
1732
1739
  const result = await collectUntilExitOrLog(candidate, "owner replacement committed")
1733
1740
 
1734
- assert.equal(result.message, "owner replacement committed", result.output)
1741
+ expect({value: result.message, context: result.output}).toMatchObject({value: "owner replacement committed"})
1735
1742
  const status = await sendControlCommand({command: {command: "status"}, path: newSocketPath})
1736
1743
 
1737
- assert.equal(status.activeReleaseId, "v1")
1738
- assert.equal(status.generationTransition, undefined)
1739
- assert.equal(await fs.readFile(lifecycleLogPath, "utf8"), "activate:v1\nretire:v1\nretire:v2\nactivate:v1\n")
1744
+ expect(status.activeReleaseId).toBe("v1")
1745
+ expect(status.generationTransition).toBe(undefined)
1746
+ expect(await fs.readFile(lifecycleLogPath, "utf8")).toBe("activate:v1\nretire:v1\nretire:v2\nactivate:v1\n")
1740
1747
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: newSocketPath})
1741
1748
 
1742
1749
  await Promise.all([v1Path, v2Path].map((releasePath) => fs.writeFile(path.join(releasePath, "worker.fifo"), "drained\n")))
@@ -1775,9 +1782,9 @@ test("owner replacement admits only the unresolved transition's exact retained c
1775
1782
  ownerConfigDigest(replacementConfig)
1776
1783
  )
1777
1784
 
1778
- assert.equal(admitted(exactConfig), true)
1779
- assert.equal(admitted(unrelatedConfig), false)
1780
- assert.equal(admitted(exactConfig, "/srv/releases/wrong"), false)
1785
+ expect(admitted(exactConfig)).toBe(true)
1786
+ expect(admitted(unrelatedConfig)).toBe(false)
1787
+ expect(admitted(exactConfig, "/srv/releases/wrong")).toBe(false)
1781
1788
  })
1782
1789
 
1783
1790
  test("owner replacement preserves accepted degraded incumbent web authority", async () => {
@@ -1813,11 +1820,11 @@ test("owner replacement preserves accepted degraded incumbent web authority", as
1813
1820
  owner = spawn(process.execPath, [binPath, "daemon", "--config", configPath], {stdio: ["ignore", "pipe", "pipe"]})
1814
1821
  await waitForLog(owner, "control socket listening")
1815
1822
  await sendControlCommand({command: {command: "deploy", releaseId: "v1", releasePath: v1Path, revision: "v1"}, path: socketPath})
1816
- await assert.rejects(sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: socketPath}), /pre-commit compensation failed.*status 26/i)
1823
+ await expect(sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: socketPath})).rejects.toThrow(/pre-commit compensation failed.*status 26/i)
1817
1824
  const before = await sendControlCommand({command: {command: "status"}, path: socketPath})
1818
1825
  const webPid = releaseProcessPid(before, "v1", "web")
1819
1826
 
1820
- assert.equal(/** @type {{releaseId: string, state: string}[]} */ (before.releases).find(({releaseId}) => releaseId === "v2")?.state, "draining")
1827
+ expect(/** @type {{releaseId: string, state: string}[]} */ (before.releases).find(({releaseId}) => releaseId === "v2")?.state).toBe("draining")
1821
1828
  const recovery = await sendControlCommand({command: {
1822
1829
  acceptRetiredIncumbent: true,
1823
1830
  command: "recover-generation-transition",
@@ -1826,23 +1833,23 @@ test("owner replacement preserves accepted degraded incumbent web authority", as
1826
1833
  releasePath: v2Path,
1827
1834
  revision: "v2"
1828
1835
  }, path: socketPath})
1836
+ await waitForReleaseState(socketPath, "v2", "stopped")
1829
1837
  const accepted = await sendControlCommand({command: {command: "status"}, path: socketPath})
1830
1838
 
1831
- assert.equal(recovery.jobsStatus, "degraded")
1832
- assert.equal(/** @type {{releaseId: string, state: string}[]} */ (accepted.releases).find(({releaseId}) => releaseId === "v2")?.state, "stopped")
1833
- assert.equal(/** @type {{phase?: string}} */ (accepted.generationTransition).phase, "degraded_active")
1839
+ expect(recovery.jobsStatus).toBe("degraded")
1840
+ expect(/** @type {{releaseId: string, state: string}[]} */ (accepted.releases).find(({releaseId}) => releaseId === "v2")?.state).toBe("stopped")
1841
+ expect(/** @type {{phase?: string}} */ (accepted.generationTransition).phase).toBe("degraded_active")
1834
1842
  replacement = spawn(process.execPath, [binPath, "daemon", "--config", configPath, "--replace-owner"], {stdio: ["ignore", "pipe", "pipe"]})
1835
1843
  await waitForLog(replacement, "owner replacement committed")
1836
- await new Promise((resolve) => setTimeout(resolve, 250))
1837
1844
  const recovered = await sendControlCommand({command: {command: "status"}, path: socketPath})
1838
1845
  const response = await fetch(`http://127.0.0.1:${proxyPort}/release`)
1839
1846
 
1840
- assert.equal(recovered.activeReleaseId, "v1")
1841
- assert.equal(/** @type {{phase?: string}} */ (recovered.generationTransition).phase, "degraded_active")
1842
- assert.equal(releaseProcessPid(recovered, "v1", "web"), webPid)
1843
- assert.equal(response.status, 200)
1844
- assert.equal((await response.text()).trim(), "v1")
1845
- assert.equal(await fs.readFile(lifecycleLogPath, "utf8"), "activate:v1\nretire:v1\nretire:v2\n")
1847
+ expect(recovered.activeReleaseId).toBe("v1")
1848
+ expect(/** @type {{phase?: string}} */ (recovered.generationTransition).phase).toBe("degraded_active")
1849
+ expect(releaseProcessPid(recovered, "v1", "web")).toBe(webPid)
1850
+ expect(response.status).toBe(200)
1851
+ expect((await response.text()).trim()).toBe("v1")
1852
+ expect(await fs.readFile(lifecycleLogPath, "utf8")).toBe("activate:v1\nretire:v1\nretire:v2\n")
1846
1853
  } finally {
1847
1854
  for (const child of [owner, replacement]) if (child && child.exitCode === null && child.signalCode === null) child.kill("SIGKILL")
1848
1855
  await stopGuardian(statePath)
@@ -1871,8 +1878,8 @@ test("replacement publishes an unchanged control path only after incumbent retir
1871
1878
  candidate = spawn(process.execPath, [binPath, "daemon", "--config", configPath, "--replace-owner"], {stdio: ["ignore", "pipe", "pipe"]})
1872
1879
  const output = await collectUntilExitOrLog(candidate, "owner replacement committed")
1873
1880
 
1874
- assert.equal(output.message, "owner replacement committed", output.output)
1875
- assert.equal((await sendControlCommand({command: {command: "status"}, path: socketPath})).activeReleaseId, "v1")
1881
+ expect({value: output.message, context: output.output}).toMatchObject({value: "owner replacement committed"})
1882
+ expect((await sendControlCommand({command: {command: "status"}, path: socketPath})).activeReleaseId).toBe("v1")
1876
1883
  const shutdown = sendControlCommand({command: {command: "shutdown"}, path: socketPath})
1877
1884
  await fs.writeFile(path.join(releasePath, "worker.fifo"), "drained\n")
1878
1885
  await shutdown
@@ -1908,24 +1915,21 @@ test("prepared replacement fences incumbent mutations until abort", async () =>
1908
1915
  await transactionClient.connect()
1909
1916
  const prepared = await transactionClient.prepareOwnerReplacement(authority, {...authority, configDigest: "candidate-authority"})
1910
1917
 
1911
- await assert.rejects(
1912
- sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: socketPath})
1913
- .then((response) => {
1914
- v2Started = true
1915
- return response
1916
- }),
1917
- /replacement.*prepared|mutation.*fenced/i
1918
- )
1918
+ await expect(sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: socketPath})
1919
+ .then((response) => {
1920
+ v2Started = true
1921
+ return response
1922
+ })).rejects.toThrow(/replacement.*prepared|mutation.*fenced/i)
1919
1923
  const retained = await sendControlCommand({command: {command: "status"}, path: socketPath})
1920
- assert.equal(retained.activeReleaseId, "v1")
1921
- assert.deepEqual(retained.releaseReferences, [{releaseId: "v1", releasePath: v1Path}])
1924
+ expect(retained.activeReleaseId).toBe("v1")
1925
+ expect(retained.releaseReferences).toEqual([{releaseId: "v1", releasePath: v1Path}])
1922
1926
  await transactionClient.abortOwnerReplacement(prepared.replacementId)
1923
1927
  await sendControlCommand({command: {command: "deploy", releaseId: "v2", releasePath: v2Path, revision: "v2"}, path: socketPath})
1924
1928
  v2Started = true
1925
1929
  const afterAbort = await sendControlCommand({command: {command: "status"}, path: socketPath})
1926
1930
 
1927
- assert.equal(afterAbort.activeReleaseId, "v2")
1928
- assert.deepEqual(afterAbort.releaseReferences, [
1931
+ expect(afterAbort.activeReleaseId).toBe("v2")
1932
+ expect(afterAbort.releaseReferences).toEqual([
1929
1933
  {releaseId: "v1", releasePath: v1Path},
1930
1934
  {releaseId: "v2", releasePath: v2Path}
1931
1935
  ])
@@ -2008,8 +2012,8 @@ async function prepareCandidatePackage(destination, options = {}) {
2008
2012
  const sessionMarker = " this.socket.write(`${JSON.stringify(command)}\\n`)\n return await response\n"
2009
2013
  const sessionInjection = " this.socket.write(`${JSON.stringify(command)}\\n`)\n const result = await response\n if (command.command === \"commit-owner-replacement\") throw new Error(\"injected lost commit response\")\n return result\n"
2010
2014
 
2011
- assert.ok(source.includes(marker))
2012
- assert.ok(source.includes(sessionMarker))
2015
+ expect(source.includes(marker)).toBeTruthy()
2016
+ expect(source.includes(sessionMarker)).toBeTruthy()
2013
2017
  await fs.writeFile(clientPath, source.replace(marker, injected).replace(sessionMarker, sessionInjection))
2014
2018
  }
2015
2019
  }
@@ -2033,15 +2037,15 @@ async function removeDaemonRecoveryCapability(packagePath, {abortedPath, prepare
2033
2037
 
2034
2038
  `
2035
2039
 
2036
- assert.ok(source.includes(capability))
2037
- assert.ok(source.includes(incumbentAbortNotification))
2040
+ expect(source.includes(capability)).toBeTruthy()
2041
+ expect(source.includes(incumbentAbortNotification)).toBeTruthy()
2038
2042
  await fs.writeFile(guardianPath, source.replace(capability, legacyCapability))
2039
2043
  const daemonSource = await fs.readFile(daemonPath, "utf8")
2040
2044
  const preparedHandler = " this.guardian.onEvent(\"replacement-prepared\", () => {\n for (const release of this.releases.values()) release.pauseDrainForOwnerHandoff()\n"
2041
2045
  const abortedHandler = " this.guardian.onEvent(\"replacement-aborted\", () => {\n"
2042
2046
 
2043
- assert.ok(daemonSource.includes(preparedHandler))
2044
- assert.ok(daemonSource.includes(abortedHandler))
2047
+ expect(daemonSource.includes(preparedHandler)).toBeTruthy()
2048
+ expect(daemonSource.includes(abortedHandler)).toBeTruthy()
2045
2049
  await fs.writeFile(daemonPath, daemonSource
2046
2050
  .replace(preparedHandler, `${preparedHandler} void fs.writeFile(${JSON.stringify(preparedPath)}, "paused\\n")\n`)
2047
2051
  .replace(abortedHandler, `${abortedHandler} void fs.writeFile(${JSON.stringify(abortedPath)}, "aborted\\n")\n`))
@@ -2108,7 +2112,7 @@ async function startPartialGuardian({backendPath, mode, socketPath, token}) {
2108
2112
  })
2109
2113
  })
2110
2114
  if (child.connected) await once(child, "disconnect")
2111
- assert.ok(child.pid)
2115
+ if (!child.pid) throw new Error("Missing required fixture: child.pid")
2112
2116
  return child
2113
2117
  }
2114
2118
 
@@ -2138,7 +2142,7 @@ async function closeServer(server) {
2138
2142
  */
2139
2143
  async function makeFifo(fifoPath) {
2140
2144
  const child = spawn("mkfifo", [fifoPath])
2141
- assert.equal((await once(child, "exit"))[0], 0)
2145
+ expect((await once(child, "exit"))[0]).toBe(0)
2142
2146
  }
2143
2147
 
2144
2148
  /**
@@ -2224,7 +2228,7 @@ async function run(command, args) {
2224
2228
  child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk })
2225
2229
  child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk })
2226
2230
  const [code] = await once(child, "exit")
2227
- assert.ok(child.pid)
2231
+ if (!child.pid) throw new Error("Missing required fixture: child.pid")
2228
2232
  return {code, pid: child.pid, stderr, stdout}
2229
2233
  }
2230
2234
 
@@ -2234,8 +2238,8 @@ async function run(command, args) {
2234
2238
  * @returns {Promise<void>} Resolves after the matching log event.
2235
2239
  */
2236
2240
  async function waitForLog(child, message) {
2237
- assert.ok(child.stdout)
2238
- assert.ok(child.stderr)
2241
+ if (!child.stdout) throw new Error("Missing required fixture: child.stdout")
2242
+ if (!child.stderr) throw new Error("Missing required fixture: child.stderr")
2239
2243
  child.stdout.setEncoding("utf8")
2240
2244
  child.stderr.setEncoding("utf8")
2241
2245
  await new Promise((resolve, reject) => {
@@ -2270,8 +2274,8 @@ async function waitForLog(child, message) {
2270
2274
  * @returns {Promise<{message?: string, output: string}>} Exit output or matched message.
2271
2275
  */
2272
2276
  async function collectUntilExitOrLog(child, message) {
2273
- assert.ok(child.stdout)
2274
- assert.ok(child.stderr)
2277
+ if (!child.stdout) throw new Error("Missing required fixture: child.stdout")
2278
+ if (!child.stderr) throw new Error("Missing required fixture: child.stderr")
2275
2279
  const stdout = child.stdout
2276
2280
  const stderr = child.stderr
2277
2281
 
@@ -2325,7 +2329,7 @@ async function openWebSocket(port) {
2325
2329
  ].join("\r\n"))
2326
2330
  const [response] = await once(socket, "data")
2327
2331
 
2328
- assert.match(String(response), /^HTTP\/1\.1 101 /)
2332
+ expect(String(response)).toMatch(/^HTTP\/1\.1 101 /)
2329
2333
  return socket
2330
2334
  }
2331
2335
 
@@ -2372,3 +2376,4 @@ async function stopGuardian(statePath) {
2372
2376
  if (!error || typeof error !== "object" || !("code" in error) || !["ENOENT", "ESRCH"].includes(String(error.code))) throw error
2373
2377
  }
2374
2378
  }
2379
+ })