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,16 +1,17 @@
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 os from "node:os"
8
7
  import path from "node:path"
9
- import test from "node:test"
8
+ import {describe, expect, test} from "@velocious/testing"
10
9
  import {fileURLToPath} from "node:url"
11
10
  import GuardianClient from "../src/guardian-client.js"
12
11
  import {waitForProcessExit} from "./support/process.js"
13
12
 
13
+ describe("guardian-client", () => {
14
+
14
15
  const legacyGuardianPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures", "pre-split3-process-guardian.js")
15
16
  const recoveryOwnerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures", "guardian-recovery-owner.js")
16
17
 
@@ -18,17 +19,21 @@ test("guardian bootstrap capability is absent from process argv", async () => {
18
19
  const fixture = await createGuardian()
19
20
 
20
21
  try {
21
- assert.deepEqual(await fixture.client.capabilities(), {daemonRecovery: 1, generationReactivation: 1})
22
+ expect(await fixture.client.capabilities()).toEqual({daemonRecovery: 1, generationReactivation: 1})
22
23
  const commandLine = await fs.readFile(`/proc/${fixture.client.pid}/cmdline`, "utf8")
23
24
  const environment = await fs.readFile(`/proc/${fixture.client.pid}/environ`, "utf8")
24
25
  const status = await fs.readFile(`/proc/${fixture.client.pid}/status`, "utf8")
25
26
  const inventory = JSON.stringify(await fixture.client.inventory())
26
27
 
27
- assert.ok(commandLine.includes("process-guardian.js"))
28
- assert.ok(!commandLine.includes(fixture.token), "guardian capability must not be exposed through argv")
29
- assert.ok(!environment.includes(fixture.token), "guardian capability must not be exposed through env")
30
- assert.ok(!status.includes(fixture.token), "guardian capability must not be exposed through process status/title")
31
- assert.ok(!inventory.includes(fixture.token), "guardian capability must not be exposed through guardian status")
28
+ expect(commandLine.includes("process-guardian.js")).toBeTruthy()
29
+ // Guardian capability must not be exposed through argv.
30
+ expect(commandLine.includes(fixture.token)).toBe(false)
31
+ // Guardian capability must not be exposed through env.
32
+ expect(environment.includes(fixture.token)).toBe(false)
33
+ // Guardian capability must not be exposed through process status/title.
34
+ expect(status.includes(fixture.token)).toBe(false)
35
+ // Guardian capability must not be exposed through guardian status.
36
+ expect(inventory.includes(fixture.token)).toBe(false)
32
37
  } finally {
33
38
  await cleanupGuardian(fixture)
34
39
  }
@@ -43,11 +48,11 @@ test("guardian inventory removes only an exact owned provenance", async () => {
43
48
  const inventory = await fixture.client.inventory()
44
49
  const candidate = inventory.find((entry) => entry.key === "candidate")
45
50
 
46
- assert.ok(candidate)
47
- await assert.rejects(() => fixture.client.remove("candidate", `${candidate.provenance}-wrong`), /provenance mismatch/)
48
- assert.equal((await fixture.client.inventory()).length, 1)
51
+ if (!candidate) throw new Error("Missing required fixture: candidate")
52
+ await expect(fixture.client.remove("candidate", `${candidate.provenance}-wrong`)).rejects.toThrow(/provenance mismatch/)
53
+ expect((await fixture.client.inventory()).length).toBe(1)
49
54
  await fixture.client.remove("candidate", candidate.provenance)
50
- assert.deepEqual(await fixture.client.inventory(), [])
55
+ expect(await fixture.client.inventory()).toEqual([])
51
56
  } finally {
52
57
  await cleanupGuardian(fixture)
53
58
  }
@@ -64,7 +69,22 @@ test("guardian runs a strict activation lifecycle command for the exact register
64
69
  try {
65
70
  await processInstance.start()
66
71
  await processInstance.activateStrict()
67
- assert.equal(await fs.readFile(activationPath, "utf8"), "activated")
72
+ expect(await fs.readFile(activationPath, "utf8")).toBe("activated")
73
+ } finally {
74
+ await cleanupGuardian(fixture)
75
+ }
76
+ })
77
+
78
+ test("guardian preserves a custom activation timeout in the registered process definition", async () => {
79
+ const fixture = await createGuardian()
80
+ const processInstance = fixture.client.process("candidate-activation-timeout", {
81
+ ...definition("candidate-activation-timeout"),
82
+ lifecycle: {activateCommand: "sleep 0.05", activateTimeoutMs: 10, drainTimeoutMs: 0}
83
+ })
84
+
85
+ try {
86
+ await processInstance.start()
87
+ await expect(processInstance.activateStrict()).rejects.toThrow(/activate command timed out after 10ms/i)
68
88
  } finally {
69
89
  await cleanupGuardian(fixture)
70
90
  }
@@ -97,20 +117,20 @@ test("client reactivates a retained process through a guardian without the react
97
117
  await processInstance.quiesceStrict()
98
118
  await processInstance.reactivateStrict()
99
119
 
100
- assert.equal(processInstance.status().pid, pid)
101
- assert.equal(processInstance.status().state, "running")
102
- assert.equal(processInstance.status().lifecycleRole, "active")
103
- assert.equal(await fs.readFile(lifecyclePath, "utf8"), "activate\nretire\nactivate\n")
120
+ expect(processInstance.status().pid).toBe(pid)
121
+ expect(processInstance.status().state).toBe("running")
122
+ expect(processInstance.status().lifecycleRole).toBe("active")
123
+ expect(await fs.readFile(lifecyclePath, "utf8")).toBe("activate\nretire\nactivate\n")
104
124
 
105
125
  const restarted = once(processInstance, "started")
106
126
 
107
- assert.ok(pid)
127
+ if (!pid) throw new Error("Missing required fixture: pid")
108
128
  process.kill(-pid, "SIGKILL")
109
129
  await restarted
110
- assert.notEqual(processInstance.status().pid, pid)
111
- assert.equal(processInstance.status().state, "running")
112
- assert.equal(processInstance.status().lifecycleRole, "active")
113
- assert.equal(await fs.readFile(lifecyclePath, "utf8"), "activate\nretire\nactivate\nactivate\n")
130
+ expect(processInstance.status().pid).not.toBe(pid)
131
+ expect(processInstance.status().state).toBe("running")
132
+ expect(processInstance.status().lifecycleRole).toBe("active")
133
+ expect(await fs.readFile(lifecyclePath, "utf8")).toBe("activate\nretire\nactivate\nactivate\n")
114
134
  } finally {
115
135
  await cleanupGuardian(fixture)
116
136
  }
@@ -142,10 +162,10 @@ test("client reverses a worker quiet hook through a pre-reactivation guardian",
142
162
  await processInstance.quiesceStrict()
143
163
  await processInstance.reactivateStrict()
144
164
 
145
- assert.equal(processInstance.status().pid, pid)
146
- assert.equal(processInstance.status().state, "running")
147
- assert.equal(processInstance.status().lifecycleRole, "active")
148
- assert.equal(await fs.readFile(lifecyclePath, "utf8"), "quiet\nresume\n")
165
+ expect(processInstance.status().pid).toBe(pid)
166
+ expect(processInstance.status().state).toBe("running")
167
+ expect(processInstance.status().lifecycleRole).toBe("active")
168
+ expect(await fs.readFile(lifecyclePath, "utf8")).toBe("quiet\nresume\n")
149
169
  } finally {
150
170
  await cleanupGuardian(fixture)
151
171
  }
@@ -163,8 +183,8 @@ test("guardian atomically updates process provenance with private owner state",
163
183
  const previousProvenance = (await fixture.client.inventory())[0]?.provenance
164
184
 
165
185
  await processInstance.updateDefinition({...definition("service"), env: {RELEASE: "v2"}}, nextOwnerState)
166
- assert.deepEqual(await fixture.client.ownerState(), nextOwnerState)
167
- assert.notEqual((await fixture.client.inventory())[0]?.provenance, previousProvenance)
186
+ expect(await fixture.client.ownerState()).toEqual(nextOwnerState)
187
+ expect((await fixture.client.inventory())[0]?.provenance).not.toBe(previousProvenance)
168
188
  } finally {
169
189
  await cleanupGuardian(fixture)
170
190
  }
@@ -186,9 +206,10 @@ test("guardian forwards each retained output line to its exact process proxy", a
186
206
  const [entry] = await Promise.race([logged, exitedFirst])
187
207
  const event = await forwarded
188
208
 
189
- assert.equal(entry.line, marker)
190
- assert.equal(event.status, undefined, "log events must not resend the complete retained process status")
191
- assert.ok(processInstance.status().logs.some((candidate) => candidate.line === marker))
209
+ expect(entry.line).toBe(marker)
210
+ // Log events must not resend the complete retained process status.
211
+ expect(event.status).toBe(undefined)
212
+ expect(processInstance.status().logs.some((candidate) => candidate.line === marker)).toBeTruthy()
192
213
  } finally {
193
214
  await cleanupGuardian(fixture)
194
215
  }
@@ -204,12 +225,12 @@ test("guardian delivers the final process status after dropping logs for a backp
204
225
  })
205
226
  const socket = fixture.client.socket
206
227
 
207
- assert.ok(socket)
228
+ if (!socket) throw new Error("Missing required fixture: socket")
208
229
  try {
209
230
  await processInstance.start()
210
231
  const pid = processInstance.status().pid
211
232
 
212
- assert.ok(pid)
233
+ if (!pid) throw new Error("Missing required fixture: pid")
213
234
  const finalStatus = fixture.client.waitForEvent("process")
214
235
 
215
236
  socket.pause()
@@ -227,7 +248,7 @@ test("guardian delivers the final process status after dropping logs for a backp
227
248
  } finally {
228
249
  clearTimeout(timeout)
229
250
  }
230
- assert.equal(processInstance.status().state, "failed")
251
+ expect(processInstance.status().state).toBe("failed")
231
252
  } finally {
232
253
  socket.resume()
233
254
  await cleanupGuardian(fixture)
@@ -247,7 +268,7 @@ test("guardian resynchronizes retained logs after dropping output for a backpres
247
268
  })
248
269
  const socket = fixture.client.socket
249
270
 
250
- assert.ok(socket)
271
+ if (!socket) throw new Error("Missing required fixture: socket")
251
272
  try {
252
273
  await processInstance.start()
253
274
  socket.pause()
@@ -267,7 +288,7 @@ test("guardian resynchronizes retained logs after dropping output for a backpres
267
288
  } finally {
268
289
  clearTimeout(timeout)
269
290
  }
270
- assert.equal(processInstance.status().logs.at(-1)?.line, finalLine)
291
+ expect(processInstance.status().logs.at(-1)?.line).toBe(finalLine)
271
292
  } finally {
272
293
  socket.resume()
273
294
  await cleanupGuardian(fixture)
@@ -283,13 +304,14 @@ test("guardian shutdown reports an exact owned process stop failure", async () =
283
304
  await processInstance.start()
284
305
  const [entry] = await fixture.client.inventory()
285
306
 
286
- assert.ok(entry)
307
+ if (!entry) throw new Error("Missing required fixture: entry")
287
308
  fixture.client.disconnect()
288
309
  await replacement.connect()
289
310
  await replacement.claimOwner(0, null)
290
- await assert.rejects(() => replacement.reconcileInventory(), /NOT_A_SIGNAL|Unknown signal/)
291
- assert.deepEqual((await replacement.inventory()).map(({key, provenance}) => ({key, provenance})), [{key: entry.key, provenance: entry.provenance}], "failed reconciliation must retain the exact registration")
292
- await assert.rejects(() => replacement.shutdown(), /NOT_A_SIGNAL|Unknown signal/)
311
+ await expect(replacement.reconcileInventory()).rejects.toThrow(/NOT_A_SIGNAL|Unknown signal/)
312
+ // Failed reconciliation must retain the exact registration.
313
+ expect((await replacement.inventory()).map(({key, provenance}) => ({key, provenance}))).toEqual([{key: entry.key, provenance: entry.provenance}])
314
+ await expect(replacement.shutdown()).rejects.toThrow(/NOT_A_SIGNAL|Unknown signal/)
293
315
  } finally {
294
316
  const inventory = await replacement.inventory().catch(() => [])
295
317
  const entry = inventory[0]
@@ -312,8 +334,8 @@ test("successful guardian shutdown closes an authenticated waiting contender bef
312
334
 
313
335
  try {
314
336
  await contender.connect()
315
- assert.ok(fixture.client.socket)
316
- assert.ok(contender.socket)
337
+ if (!fixture.client.socket) throw new Error("Missing required fixture: fixture.client.socket")
338
+ if (!contender.socket) throw new Error("Missing required fixture: contender.socket")
317
339
  const shutdownOrder = /** @type {string[]} */ ([])
318
340
  const onData = fixture.client.onData.bind(fixture.client)
319
341
 
@@ -324,14 +346,17 @@ test("successful guardian shutdown closes an authenticated waiting contender bef
324
346
  fixture.client.socket.once("close", () => shutdownOrder.push("caller-close"))
325
347
  const contenderClosed = new Promise((resolve) => contender.socket?.once("close", () => resolve(undefined)))
326
348
  const waitingClaim = contender.claimOwner(250, null)
327
- const rejectedClaim = assert.rejects(waitingClaim, /connection closed/)
349
+ const rejectedClaim = (async () => {
350
+ await expect(waitingClaim).rejects.toThrow(/connection closed/)
351
+ })()
328
352
 
329
353
  await fixture.client.shutdown()
330
354
  await rejectedClaim
331
355
  await contenderClosed
332
- assert.deepEqual(shutdownOrder, ["response", "caller-close"], "shutdown success must be received before the caller connection closes")
333
- await assert.rejects(() => contender.inventory(), /not connected/)
334
- await assert.rejects(fs.access(fixture.client.socketPath), {code: "ENOENT"})
356
+ // Shutdown success must be received before the caller connection closes.
357
+ expect(shutdownOrder).toEqual(["response", "caller-close"])
358
+ await expect(contender.inventory()).rejects.toThrow(/not connected/)
359
+ await expect(fs.access(fixture.client.socketPath)).rejects.toMatchObject({code: "ENOENT"})
335
360
  await fixture.client.guardianExit()
336
361
  } finally {
337
362
  contender.disconnect()
@@ -355,7 +380,7 @@ test("guardian restart uses the latest accepted command and exact environment",
355
380
 
356
381
  const [restart] = await waitForRestartRecords(markerPath, 1)
357
382
 
358
- assert.deepEqual({home: restart.home, marker: restart.marker}, {home: null, marker: "new"})
383
+ expect({home: restart.home, marker: restart.marker}).toEqual({home: null, marker: "new"})
359
384
  } finally {
360
385
  await reconnectAndShutdownGuardian(fixture, authority)
361
386
  }
@@ -375,7 +400,7 @@ test("guardian rearms recovery after an ownerless replacement aborts", async ()
375
400
 
376
401
  await new Promise((resolve) => setTimeout(resolve, 80))
377
402
  await replacement.abortOwnerReplacement(prepared.replacementId)
378
- assert.deepEqual((await waitForRestartRecords(markerPath, 1)).map(({home, marker}) => ({home, marker})), [{home: null, marker: "accepted"}])
403
+ expect((await waitForRestartRecords(markerPath, 1)).map(({home, marker}) => ({home, marker}))).toEqual([{home: null, marker: "accepted"}])
379
404
  } finally {
380
405
  replacement.disconnect()
381
406
  await reconnectAndShutdownGuardian(fixture, authority)
@@ -392,9 +417,9 @@ test("guardian retry backoff remains nonzero when reconnect grace is zero", asyn
392
417
  fixture.client.disconnect()
393
418
  const records = await waitForRestartRecords(markerPath, 2)
394
419
 
395
- assert.equal(typeof records[0]?.at, "number")
396
- assert.equal(typeof records[1]?.at, "number")
397
- assert.ok(Number(records[1].at) - Number(records[0].at) >= 900, `failed owner recovery retried after ${Number(records[1].at) - Number(records[0].at)}ms`)
420
+ expect(typeof records[0]?.at).toBe("number")
421
+ expect(typeof records[1]?.at).toBe("number")
422
+ expect({value: Boolean(Number(records[1].at) - Number(records[0].at) >= 900), context: `failed owner recovery retried after ${Number(records[1].at) - Number(records[0].at)}ms`}).toMatchObject({value: true})
398
423
  } finally {
399
424
  await reconnectAndShutdownGuardian(fixture, authority)
400
425
  }
@@ -421,7 +446,7 @@ test("a retired guardian-started owner does not block recovery of its replacemen
421
446
  await committed
422
447
  replacement.disconnect()
423
448
 
424
- assert.deepEqual((await waitForRestartRecords(secondMarkerPath, 1)).map(({home, marker}) => ({home, marker})), [{home: null, marker: "replacement"}])
449
+ expect((await waitForRestartRecords(secondMarkerPath, 1)).map(({home, marker}) => ({home, marker}))).toEqual([{home: null, marker: "replacement"}])
425
450
  } finally {
426
451
  replacement.disconnect()
427
452
  if (firstOwnerPid) {
@@ -544,7 +569,7 @@ test("guardian backs off when a restarted owner exits after claiming but before
544
569
  const records = await waitForRestartRecords(startedLogPath, 2)
545
570
 
546
571
  await waitForProcessExit(descendantPid)
547
- assert.ok(Number(records[1].at) - Number(records[0].at) >= 900, `post-claim failure retried after ${Number(records[1].at) - Number(records[0].at)}ms`)
572
+ expect({value: Boolean(Number(records[1].at) - Number(records[0].at) >= 900), context: `post-claim failure retried after ${Number(records[1].at) - Number(records[0].at)}ms`}).toMatchObject({value: true})
548
573
  } finally {
549
574
  if (descendantPid) {
550
575
  try { process.kill(descendantPid, "SIGKILL") } catch (_error) { /* Exact descendant already exited. */ }
@@ -575,7 +600,7 @@ test("guardian preserves restart backoff when an unready owner disconnect aborts
575
600
  await aborted
576
601
  const records = await waitForRestartRecords(startedLogPath, 2)
577
602
 
578
- assert.ok(Number(records[1].at) - Number(records[0].at) >= 900, `replacement abort retried after ${Number(records[1].at) - Number(records[0].at)}ms`)
603
+ expect({value: Boolean(Number(records[1].at) - Number(records[0].at) >= 900), context: `replacement abort retried after ${Number(records[1].at) - Number(records[0].at)}ms`}).toMatchObject({value: true})
579
604
  } finally {
580
605
  replacement.disconnect()
581
606
  await reconnectAndShutdownGuardian(fixture, authority)
@@ -614,7 +639,8 @@ test("replacement commit preserves a claimed guardian restart child through list
614
639
  process.kill(incumbentPid, "SIGUSR2")
615
640
  await Promise.race([waitForFileText(committedPath, new RegExp(prepared.replacementId)), published])
616
641
  await new Promise((resolve) => setTimeout(resolve, 50))
617
- assert.doesNotThrow(() => process.kill(incumbentPid, 0), "claimed incumbent must survive replacement commit until its listeners retire")
642
+ // The claimed incumbent must survive replacement commit until its listeners retire.
643
+ await expect(() => process.kill(incumbentPid, 0)).not.toThrow()
618
644
 
619
645
  process.kill(incumbentPid, "SIGUSR1")
620
646
  await published
@@ -655,7 +681,7 @@ test("ownerless replacement commit kills a superseded restart candidate", async
655
681
  await committed
656
682
  replacement.disconnect()
657
683
 
658
- assert.deepEqual((await waitForRestartRecords(secondMarkerPath, 1)).map(({home, marker}) => ({home, marker})), [{home: null, marker: "committed"}])
684
+ expect((await waitForRestartRecords(secondMarkerPath, 1)).map(({home, marker}) => ({home, marker}))).toEqual([{home: null, marker: "committed"}])
659
685
  } finally {
660
686
  replacement.disconnect()
661
687
  if (delayedOwnerPid) {
@@ -693,9 +719,10 @@ test("guardian logs an asynchronous daemon spawn failure before retrying", async
693
719
  const diagnosticPattern = /"code":"ENOENT".*"message":"guardian failed to restart daemon"/
694
720
  const diagnostic = await waitForFileText(logPath, diagnosticPattern)
695
721
 
696
- assert.match(diagnostic, diagnosticPattern)
722
+ expect(diagnostic).toMatch(diagnosticPattern)
697
723
  for (const privateValue of [privateArgs, privateEnvironment, privateExecutable, fixture.root, logPath]) {
698
- assert.ok(!diagnostic.includes(privateValue), `guardian diagnostic exposed ${privateValue}`)
724
+ // Guardian diagnostics must not expose private values, including in assertion failures.
725
+ expect(diagnostic.includes(privateValue)).toBe(false)
699
726
  }
700
727
  } finally {
701
728
  await reconnectAndShutdownGuardian(fixture, authority)
@@ -729,8 +756,8 @@ test("guardian publishes the authenticated ready owner's PID file", async () =>
729
756
  await fs.symlink(victimPath, pidPath)
730
757
  await fixture.client.ownerReady()
731
758
 
732
- assert.equal(await fs.readFile(pidPath, "utf8"), `${process.pid}\n`)
733
- assert.equal(await fs.readFile(victimPath, "utf8"), "unchanged\n")
759
+ expect(await fs.readFile(pidPath, "utf8")).toBe(`${process.pid}\n`)
760
+ expect(await fs.readFile(victimPath, "utf8")).toBe("unchanged\n")
734
761
  } finally {
735
762
  await cleanupGuardian(fixture)
736
763
  }
@@ -752,14 +779,15 @@ test("replacement commit notification waits for incumbent listener retirement",
752
779
 
753
780
  await fixture.client.commitOwnerReplacement(prepared.replacementId)
754
781
  await new Promise((resolve) => setImmediate(resolve))
755
- assert.equal(committed, false, "candidate publication must remain fenced while incumbent listener retirement is delayed")
782
+ // Candidate publication must remain fenced while incumbent listener retirement is delayed.
783
+ expect(committed).toBe(false)
756
784
  const listenersRetired = candidate.waitForEvent("replacement-listeners-retired")
757
785
 
758
786
  await fixture.client.completeOwnerListenerRetirement(prepared.replacementId)
759
787
  await listenersRetired
760
788
  await fixture.client.request({command: "finalize-owner-replacement", replacementId: prepared.replacementId})
761
789
  await notification
762
- assert.equal(committed, true)
790
+ expect(committed).toBe(true)
763
791
  await candidate.shutdown()
764
792
  await fixture.client.guardianExit()
765
793
  } finally {
@@ -768,7 +796,7 @@ test("replacement commit notification waits for incumbent listener retirement",
768
796
  }
769
797
  })
770
798
 
771
- test("completed direct listener retirement finalizes when the incumbent disconnects", {timeout: 3000}, async () => {
799
+ test("completed direct listener retirement finalizes when the incumbent disconnects", {timeoutMs: 3000}, async () => {
772
800
  const fixture = await createGuardian()
773
801
  const candidate = new GuardianClient({socketPath: fixture.client.socketPath, token: fixture.token})
774
802
  const authority = {configDigest: "incumbent", runtime: null}
@@ -787,7 +815,7 @@ test("completed direct listener retirement finalizes when the incumbent disconne
787
815
  fixture.client.disconnect()
788
816
  await committed
789
817
  await candidate.finalizeOwnerReplacement(prepared.replacementId)
790
- assert.deepEqual(await candidate.replacementStatus(), {
818
+ expect(await candidate.replacementStatus()).toEqual({
791
819
  committedReplacementId: prepared.replacementId,
792
820
  ownerClaimed: true,
793
821
  retirementFailed: false,
@@ -814,14 +842,11 @@ test("replacement staging rejects owner state published after prepare", async ()
814
842
  const prepared = await candidate.prepareOwnerReplacement(authority, nextAuthority)
815
843
 
816
844
  await fixture.client.publishOwnerState({authority, snapshot: {activeReleaseId: "v2"}})
817
- await assert.rejects(
818
- () => candidate.stageOwnerReplacement(prepared.replacementId, {authority: nextAuthority, snapshot: {activeReleaseId: "v1"}}),
819
- /owner state changed after prepare/i
820
- )
845
+ await expect(candidate.stageOwnerReplacement(prepared.replacementId, {authority: nextAuthority, snapshot: {activeReleaseId: "v1"}})).rejects.toThrow(/owner state changed after prepare/i)
821
846
  await candidate.abortOwnerReplacement(prepared.replacementId)
822
847
  const fresh = await candidate.prepareOwnerReplacement(authority, nextAuthority)
823
848
 
824
- assert.deepEqual(fresh.ownerState, {authority, snapshot: {activeReleaseId: "v2"}})
849
+ expect(fresh.ownerState).toEqual({authority, snapshot: {activeReleaseId: "v2"}})
825
850
  await candidate.abortOwnerReplacement(fresh.replacementId)
826
851
  await fixture.client.shutdown()
827
852
  await fixture.client.guardianExit()
@@ -831,7 +856,7 @@ test("replacement staging rejects owner state published after prepare", async ()
831
856
  }
832
857
  })
833
858
 
834
- test("staged replacement receives cleared local sources when the incumbent disconnects", {timeout: 3000}, async () => {
859
+ test("staged replacement receives cleared local sources when the incumbent disconnects", {timeoutMs: 3000}, async () => {
835
860
  const fixture = await createGuardian()
836
861
  const candidate = new GuardianClient({socketPath: fixture.client.socketPath, token: fixture.token})
837
862
  const authority = {configDigest: "incumbent", runtime: null}
@@ -858,7 +883,7 @@ test("staged replacement receives cleared local sources when the incumbent disco
858
883
  const committed = candidate.waitForEvent("replacement-committed")
859
884
 
860
885
  fixture.client.disconnect()
861
- assert.deepEqual(await cleared, {
886
+ expect(await cleared).toEqual({
862
887
  connections: {http: 0, websocket: 0},
863
888
  event: "owner-connection-state",
864
889
  releaseId: "v1",
@@ -867,7 +892,7 @@ test("staged replacement receives cleared local sources when the incumbent disco
867
892
  await committed
868
893
  const state = /** @type {{listenerConnectionSources?: Record<string, Record<string, {http: number, websocket: number}>>}} */ (await candidate.ownerState())
869
894
 
870
- assert.deepEqual(state.listenerConnectionSources, {})
895
+ expect(state.listenerConnectionSources).toEqual({})
871
896
  await candidate.shutdown()
872
897
  await fixture.client.guardianExit()
873
898
  } finally {
@@ -876,7 +901,7 @@ test("staged replacement receives cleared local sources when the incumbent disco
876
901
  }
877
902
  })
878
903
 
879
- test("staged successor state receives tombstones when an older completed listener disconnects", {timeout: 3000}, async () => {
904
+ test("staged successor state receives tombstones when an older completed listener disconnects", {timeoutMs: 3000}, async () => {
880
905
  const fixture = await createGuardian()
881
906
  const candidate = new GuardianClient({socketPath: fixture.client.socketPath, token: fixture.token})
882
907
  const successor = new GuardianClient({socketPath: fixture.client.socketPath, token: fixture.token})
@@ -918,12 +943,12 @@ test("staged successor state receives tombstones when an older completed listene
918
943
  fixture.client.disconnect()
919
944
  const tombstone = {connections: {http: 0, websocket: 0}, event: "owner-connection-state", releaseId: "v1", sourceId: "retired-local"}
920
945
 
921
- assert.deepEqual(await Promise.all([sourceCleared, stagedSourceCleared]), [tombstone, tombstone])
946
+ expect(await Promise.all([sourceCleared, stagedSourceCleared])).toEqual([tombstone, tombstone])
922
947
 
923
948
  await candidate.commitOwnerReplacement(second.replacementId)
924
949
  const successorState = /** @type {{listenerConnectionSources?: Record<string, Record<string, {http: number, websocket: number}>>}} */ (await successor.ownerState())
925
950
 
926
- assert.deepEqual(successorState.listenerConnectionSources, {})
951
+ expect(successorState.listenerConnectionSources).toEqual({})
927
952
  const secondCommitted = successor.waitForEvent("replacement-committed")
928
953
  const secondListenersRetired = successor.waitForEvent("replacement-listeners-retired")
929
954
 
@@ -954,7 +979,7 @@ test("replacement abort notifies both the candidate and committed owner", async
954
979
  const candidateAborted = candidate.waitForEvent("replacement-aborted")
955
980
 
956
981
  await candidate.abortOwnerReplacement(prepared.replacementId)
957
- assert.deepEqual(await Promise.all([incumbentAborted, candidateAborted]), [
982
+ expect(await Promise.all([incumbentAborted, candidateAborted])).toEqual([
958
983
  {event: "replacement-aborted", reason: "Replacement candidate aborted the prepared transaction"},
959
984
  {event: "replacement-aborted", reason: "Replacement candidate aborted the prepared transaction"}
960
985
  ])
@@ -973,7 +998,7 @@ test("retired owner replacement commit carries its exact recovered process key",
973
998
 
974
999
  client.request = async (request) => {
975
1000
  if (!request.key) throw new Error(`Guardian ${request.command} requires a process key`)
976
- assert.deepEqual(request, {command: "commit-retired-owner-replacement", key: processKey, replacementId})
1001
+ expect(request).toEqual({command: "commit-retired-owner-replacement", key: processKey, replacementId})
977
1002
  return {committed: true}
978
1003
  }
979
1004
 
@@ -984,7 +1009,7 @@ test("guardian owner-replacement capability classification is explicit and fail
984
1009
  const current = new GuardianClient({socketPath: "/unused", token: "authenticated-capability"})
985
1010
 
986
1011
  current.request = async (request) => {
987
- assert.deepEqual(request, {command: "owner-replacement-capabilities"})
1012
+ expect(request).toEqual({command: "owner-replacement-capabilities"})
988
1013
  return {
989
1014
  commands: ["commit-retired-owner-replacement", "future-command"],
990
1015
  futureField: {supported: true},
@@ -992,7 +1017,7 @@ test("guardian owner-replacement capability classification is explicit and fail
992
1017
  version: 2
993
1018
  }
994
1019
  }
995
- assert.equal(await current.ownerReplacementProtocol(), "atomic")
1020
+ expect(await current.ownerReplacementProtocol()).toBe("atomic")
996
1021
 
997
1022
  for (const [commitDiagnostic, expected] of [
998
1023
  ["Owner replacement transaction is not the prepared candidate", "atomic"],
@@ -1007,8 +1032,8 @@ test("guardian owner-replacement capability classification is explicit and fail
1007
1032
  if (request.command === "owner-replacement-capabilities") throw new Error("Guardian owner-replacement-capabilities requires a process key")
1008
1033
  throw new Error(commitDiagnostic)
1009
1034
  }
1010
- assert.equal(await older.ownerReplacementProtocol(), expected)
1011
- assert.deepEqual(requests, [
1035
+ expect(await older.ownerReplacementProtocol()).toBe(expected)
1036
+ expect(requests).toEqual([
1012
1037
  {command: "owner-replacement-capabilities"},
1013
1038
  {command: "commit-retired-owner-replacement", replacementId: "owner-replacement-capability-probe"}
1014
1039
  ])
@@ -1024,7 +1049,7 @@ test("guardian owner-replacement capability classification is explicit and fail
1024
1049
  const ambiguous = new GuardianClient({socketPath: "/unused", token: "authenticated-capability"})
1025
1050
 
1026
1051
  ambiguous.request = fixture
1027
- await assert.rejects(() => ambiguous.ownerReplacementProtocol(), /invalid owner-replacement capability response|ambiguous retired-owner capability response/)
1052
+ await expect(ambiguous.ownerReplacementProtocol()).rejects.toThrow(/invalid owner-replacement capability response|ambiguous retired-owner capability response/)
1028
1053
  }
1029
1054
  })
1030
1055
 
@@ -1037,14 +1062,11 @@ test("reserved process recovery rejects a reconstructed definition with differen
1037
1062
  await fixture.client.process(processKey, definition("worker")).recover()
1038
1063
  const [registration] = await fixture.client.inventory()
1039
1064
 
1040
- assert.ok(registration)
1065
+ if (!registration) throw new Error("Missing required fixture: registration")
1041
1066
  await candidate.connect()
1042
1067
  candidate.reserveProcessRecovery(processKey, registration.provenance)
1043
- await assert.rejects(
1044
- () => candidate.process(processKey, definition("different-worker")).recover(),
1045
- /provenance mismatch for reserved process/
1046
- )
1047
- assert.deepEqual((await fixture.client.inventory()).map(({key}) => key), [processKey])
1068
+ await expect(candidate.process(processKey, definition("different-worker")).recover()).rejects.toThrow(/provenance mismatch for reserved process/)
1069
+ expect((await fixture.client.inventory()).map(({key}) => key)).toEqual([processKey])
1048
1070
  } finally {
1049
1071
  candidate.disconnect()
1050
1072
  await cleanupGuardian(fixture)
@@ -1077,10 +1099,7 @@ test("retired owner replacement rejects a registered process absent from committ
1077
1099
  const prepared = await candidate.prepareOwnerReplacement(authority, authority)
1078
1100
 
1079
1101
  await candidate.stageOwnerReplacement(prepared.replacementId, {authority, snapshot: candidateSnapshot})
1080
- await assert.rejects(
1081
- () => candidate.commitRetiredOwnerReplacement(prepared.replacementId, candidateProcessKey),
1082
- /process .* does not belong to the committed owner/
1083
- )
1102
+ await expect(candidate.commitRetiredOwnerReplacement(prepared.replacementId, candidateProcessKey)).rejects.toThrow(/process .* does not belong to the committed owner/)
1084
1103
  } finally {
1085
1104
  candidate.disconnect()
1086
1105
  await cleanupGuardian(fixture)
@@ -1110,14 +1129,14 @@ test("retired owner replacement requires unchanged authority and the exact contr
1110
1129
  const changed = await candidate.prepareOwnerReplacement(authority, nextAuthority)
1111
1130
 
1112
1131
  await candidate.stageOwnerReplacement(changed.replacementId, {authority: nextAuthority, snapshot})
1113
- await assert.rejects(() => candidate.commitRetiredOwnerReplacement(changed.replacementId, processKey), /unchanged owner authority/)
1132
+ await expect(candidate.commitRetiredOwnerReplacement(changed.replacementId, processKey)).rejects.toThrow(/unchanged owner authority/)
1114
1133
  await candidate.abortOwnerReplacement(changed.replacementId)
1115
1134
 
1116
1135
  const occupied = await candidate.prepareOwnerReplacement(authority, authority)
1117
1136
 
1118
1137
  await candidate.stageOwnerReplacement(occupied.replacementId, {authority, snapshot})
1119
1138
  await fs.writeFile(controlPath, "occupied\n")
1120
- await assert.rejects(() => candidate.commitRetiredOwnerReplacement(occupied.replacementId, processKey), /control socket .* still exists/)
1139
+ await expect(candidate.commitRetiredOwnerReplacement(occupied.replacementId, processKey)).rejects.toThrow(/control socket .* still exists/)
1121
1140
  await candidate.abortOwnerReplacement(occupied.replacementId)
1122
1141
 
1123
1142
  await fs.rm(controlPath)
@@ -1125,34 +1144,22 @@ test("retired owner replacement requires unchanged authority and the exact contr
1125
1144
 
1126
1145
  await candidate.stageOwnerReplacement(ready.replacementId, {authority, snapshot})
1127
1146
  await contender.connect()
1128
- await assert.rejects(
1129
- () => contender.request({command: "commit-retired-owner-replacement", key: processKey, replacementId: ready.replacementId}),
1130
- /not the prepared candidate/
1131
- )
1132
- await assert.rejects(
1133
- () => candidate.commitRetiredOwnerReplacement("stale-replacement", processKey),
1134
- /not the prepared candidate/
1135
- )
1136
- await assert.rejects(
1137
- () => candidate.commitRetiredOwnerReplacement(ready.replacementId, "release:v1:wrong"),
1138
- /process .* is not registered/
1139
- )
1147
+ await expect(contender.request({command: "commit-retired-owner-replacement", key: processKey, replacementId: ready.replacementId})).rejects.toThrow(/not the prepared candidate/)
1148
+ await expect(candidate.commitRetiredOwnerReplacement("stale-replacement", processKey)).rejects.toThrow(/not the prepared candidate/)
1149
+ await expect(candidate.commitRetiredOwnerReplacement(ready.replacementId, "release:v1:wrong")).rejects.toThrow(/process .* is not registered/)
1140
1150
  const handoffRequested = fixture.client.waitForEvent("replacement-listener-handoff-requested")
1141
1151
 
1142
1152
  void handoffRequested.catch(() => undefined)
1143
1153
 
1144
1154
  await candidate.prepareRetiredOwnerListenerHandoff(ready.replacementId, processKey)
1145
1155
  await handoffRequested
1146
- await assert.rejects(
1147
- () => contender.prepareOwnerReplacement(authority, authority),
1148
- /listener retirement is pending/
1149
- )
1156
+ await expect(contender.prepareOwnerReplacement(authority, authority)).rejects.toThrow(/listener retirement is pending/)
1150
1157
  const committed = candidate.waitForEvent("replacement-committed")
1151
1158
  const listenersRetired = candidate.waitForEvent("replacement-listeners-retired")
1152
1159
  const connectionState = candidate.waitForEvent("owner-connection-state")
1153
1160
 
1154
1161
  await fixture.client.publishOwnerConnectionState(ready.replacementId, "listener-a", "v1", {http: 1, websocket: 2}, true)
1155
- assert.deepEqual(await connectionState, {connections: {http: 1, websocket: 2}, event: "owner-connection-state", releaseId: "v1", sourceId: "listener-a"})
1162
+ expect(await connectionState).toEqual({connections: {http: 1, websocket: 2}, event: "owner-connection-state", releaseId: "v1", sourceId: "listener-a"})
1156
1163
  await fixture.client.completeOwnerListenerRetirement(ready.replacementId)
1157
1164
  await listenersRetired
1158
1165
  const retirementRequested = fixture.client.waitForEvent("replacement-retirement-requested")
@@ -1206,7 +1213,7 @@ test("completed listener retirement survives owner recovery and clears a crashed
1206
1213
  const initial = candidate.waitForEvent("owner-connection-state")
1207
1214
 
1208
1215
  await fixture.client.publishOwnerConnectionState(prepared.replacementId, "listener-a", "v1", {http: 0, websocket: 1}, true)
1209
- assert.deepEqual(await initial, {connections: {http: 0, websocket: 1}, event: "owner-connection-state", releaseId: "v1", sourceId: "listener-a"})
1216
+ expect(await initial).toEqual({connections: {http: 0, websocket: 1}, event: "owner-connection-state", releaseId: "v1", sourceId: "listener-a"})
1210
1217
  const listenersRetired = candidate.waitForEvent("replacement-listeners-retired")
1211
1218
 
1212
1219
  await fixture.client.completeOwnerListenerRetirement(prepared.replacementId)
@@ -1217,9 +1224,9 @@ test("completed listener retirement survives owner recovery and clears a crashed
1217
1224
  await recovered.claimOwner(1000, authority)
1218
1225
  const stateAfterCandidateCrash = /** @type {{listenerConnectionSources?: Record<string, Record<string, {http: number, websocket: number}>>}} */ (await recovered.ownerState())
1219
1226
 
1220
- assert.equal(stateAfterCandidateCrash.listenerConnectionSources?.["candidate-local"], undefined)
1221
- assert.deepEqual(stateAfterCandidateCrash.listenerConnectionSources?.["listener-a"], {v1: {http: 0, websocket: 1}})
1222
- assert.deepEqual(await recovered.replacementStatus(), {
1227
+ expect(stateAfterCandidateCrash.listenerConnectionSources?.["candidate-local"]).toBe(undefined)
1228
+ expect(stateAfterCandidateCrash.listenerConnectionSources?.["listener-a"]).toEqual({v1: {http: 0, websocket: 1}})
1229
+ expect(await recovered.replacementStatus()).toEqual({
1223
1230
  committedReplacementId: prepared.replacementId,
1224
1231
  ownerClaimed: true,
1225
1232
  retirementFailed: false,
@@ -1230,17 +1237,17 @@ test("completed listener retirement survives owner recovery and clears a crashed
1230
1237
  const cleared = recovered.waitForEvent("owner-connection-state")
1231
1238
 
1232
1239
  fixture.client.disconnect()
1233
- assert.deepEqual(await Promise.race([
1240
+ expect(await Promise.race([
1234
1241
  cleared,
1235
1242
  new Promise((_, reject) => {
1236
1243
  const timer = setTimeout(() => reject(new Error("Recovered owner did not receive the retired source tombstone")), 500)
1237
1244
 
1238
1245
  timer.unref()
1239
1246
  })
1240
- ]), {connections: {http: 0, websocket: 0}, event: "owner-connection-state", releaseId: "v1", sourceId: "listener-a"})
1247
+ ])).toEqual({connections: {http: 0, websocket: 0}, event: "owner-connection-state", releaseId: "v1", sourceId: "listener-a"})
1241
1248
  const recoveredOwnerState = /** @type {{listenerConnectionSources?: Record<string, Record<string, {http: number, websocket: number}>>}} */ (await recovered.ownerState())
1242
1249
 
1243
- assert.deepEqual(recoveredOwnerState.listenerConnectionSources, {})
1250
+ expect(recoveredOwnerState.listenerConnectionSources).toEqual({})
1244
1251
  await recovered.publishOwnerState({
1245
1252
  authority,
1246
1253
  listenerConnectionSources: {"listener-a": {v1: {http: 0, websocket: 1}}},
@@ -1249,7 +1256,7 @@ test("completed listener retirement survives owner recovery and clears a crashed
1249
1256
  })
1250
1257
  const afterStalePublication = /** @type {{listenerConnectionSources?: Record<string, Record<string, {http: number, websocket: number}>>}} */ (await recovered.ownerState())
1251
1258
 
1252
- assert.deepEqual(afterStalePublication.listenerConnectionSources, {})
1259
+ expect(afterStalePublication.listenerConnectionSources).toEqual({})
1253
1260
  const recoveredProcess = recovered.process(processKey, definition("worker"))
1254
1261
 
1255
1262
  await recoveredProcess.recover()
@@ -1261,7 +1268,7 @@ test("completed listener retirement survives owner recovery and clears a crashed
1261
1268
  })
1262
1269
  const afterStaleProcessUpdate = /** @type {{listenerConnectionSources?: Record<string, Record<string, {http: number, websocket: number}>>}} */ (await recovered.ownerState())
1263
1270
 
1264
- assert.deepEqual(afterStaleProcessUpdate.listenerConnectionSources, {})
1271
+ expect(afterStaleProcessUpdate.listenerConnectionSources).toEqual({})
1265
1272
  await recovered.shutdown()
1266
1273
  await fixture.client.guardianExit()
1267
1274
  } finally {
@@ -1288,7 +1295,7 @@ test("direct retired-listener source relay survives committed owner recovery", a
1288
1295
  const initial = candidate.waitForEvent("owner-connection-state")
1289
1296
 
1290
1297
  await fixture.client.publishOwnerConnectionState(prepared.replacementId, "direct-listener", "v1", {http: 0, websocket: 1}, true)
1291
- assert.deepEqual(await initial, {connections: {http: 0, websocket: 1}, event: "owner-connection-state", releaseId: "v1", sourceId: "direct-listener"})
1298
+ expect(await initial).toEqual({connections: {http: 0, websocket: 1}, event: "owner-connection-state", releaseId: "v1", sourceId: "direct-listener"})
1292
1299
  const listenersRetired = candidate.waitForEvent("replacement-listeners-retired")
1293
1300
 
1294
1301
  await fixture.client.completeOwnerListenerRetirement(prepared.replacementId)
@@ -1300,7 +1307,7 @@ test("direct retired-listener source relay survives committed owner recovery", a
1300
1307
  const cleared = recovered.waitForEvent("owner-connection-state")
1301
1308
 
1302
1309
  await fixture.client.publishOwnerConnectionState(prepared.replacementId, "direct-listener", "v1", {http: 0, websocket: 0}, true)
1303
- assert.deepEqual(await cleared, {connections: {http: 0, websocket: 0}, event: "owner-connection-state", releaseId: "v1", sourceId: "direct-listener"})
1310
+ expect(await cleared).toEqual({connections: {http: 0, websocket: 0}, event: "owner-connection-state", releaseId: "v1", sourceId: "direct-listener"})
1304
1311
  await recovered.shutdown()
1305
1312
  await fixture.client.guardianExit()
1306
1313
  } finally {
@@ -1350,17 +1357,17 @@ test("incumbent listener disconnect before state completion aborts without commi
1350
1357
  await fixture.client.publishOwnerConnectionState(prepared.replacementId, "incumbent-local", "v1", {http: 0, websocket: 1}, true)
1351
1358
  await sourcePublished
1352
1359
  fixture.client.disconnect()
1353
- assert.deepEqual(await failed, {
1360
+ expect(await failed).toEqual({
1354
1361
  event: "replacement-retirement-failed",
1355
1362
  reason: "Incumbent listener disconnected during the prepared handoff",
1356
1363
  replacementId: prepared.replacementId
1357
1364
  })
1358
- assert.deepEqual(await aborted, {
1365
+ expect(await aborted).toEqual({
1359
1366
  event: "replacement-aborted",
1360
1367
  reason: "Incumbent listener disconnected during the prepared handoff"
1361
1368
  })
1362
- assert.equal(tombstones, 1)
1363
- assert.deepEqual(await candidate.replacementStatus(), {
1369
+ expect(tombstones).toBe(1)
1370
+ expect(await candidate.replacementStatus()).toEqual({
1364
1371
  committedReplacementId: null,
1365
1372
  ownerClaimed: false,
1366
1373
  retirementFailed: false,
@@ -1402,7 +1409,7 @@ test("replacement abort during listener handoff validation leaves the incumbent
1402
1409
  const abandonedHandoff = candidate.prepareRetiredOwnerListenerHandoff(prepared.replacementId, processKey)
1403
1410
 
1404
1411
  await candidate.abortOwnerReplacement(prepared.replacementId)
1405
- await assert.rejects(() => abandonedHandoff, /prepared candidate/)
1412
+ await expect(abandonedHandoff).rejects.toThrow(/prepared candidate/)
1406
1413
  await contender.connect()
1407
1414
  const fresh = await contender.prepareOwnerReplacement(authority, authority)
1408
1415
 
@@ -1452,7 +1459,7 @@ test("queued owner claim is revalidated against the latest committed authority",
1452
1459
  await new Promise((resolve) => setTimeout(resolve, 20))
1453
1460
  await fixture.client.publishOwnerState({authority: nextAuthority, snapshot: {activeReleaseId: null}})
1454
1461
  fixture.client.disconnect()
1455
- await assert.rejects(claim, /authority changed while the claim was queued/)
1462
+ await expect(claim).rejects.toThrow(/authority changed while the claim was queued/)
1456
1463
  await contender.claimOwner(500, nextAuthority)
1457
1464
  await contender.shutdown()
1458
1465
  await fixture.client.guardianExit()
@@ -1471,7 +1478,7 @@ test("first upgrade migrates a real pre-split guardian without replacing its own
1471
1478
 
1472
1479
  try {
1473
1480
  await legacyProcess.start()
1474
- await assert.rejects(() => fixture.client.capabilities(), /Guardian capabilities requires a process key/)
1481
+ await expect(fixture.client.capabilities()).rejects.toThrow(/Guardian capabilities requires a process key/)
1475
1482
  legacyPid = legacyProcess.status().pid
1476
1483
  const authority = {configDigest: "legacy-config", runtime: {digest: "legacy-runtime", format: 1, path: "/legacy", version: "0.1.28"}}
1477
1484
  const nextAuthority = {...authority, runtime: {...authority.runtime, digest: "candidate-runtime", path: "/candidate"}}
@@ -1485,7 +1492,7 @@ test("first upgrade migrates a real pre-split guardian without replacing its own
1485
1492
  }
1486
1493
  }
1487
1494
 
1488
- assert.ok(legacyPid)
1495
+ if (!legacyPid) throw new Error("Missing required fixture: legacyPid")
1489
1496
  upgraded = await fixture.client.upgradeLegacyGuardian({
1490
1497
  ownerState,
1491
1498
  socketPath: path.join(fixture.root, "guardian-v2.sock"),
@@ -1496,11 +1503,19 @@ test("first upgrade migrates a real pre-split guardian without replacing its own
1496
1503
  const restored = upgraded.process("release:v1:legacy-worker", processDefinition)
1497
1504
 
1498
1505
  await restored.recover()
1499
- assert.equal(restored.status().pid, legacyPid)
1500
- assert.deepEqual(prepared.ownerState, ownerState)
1501
- assert.deepEqual(await upgraded.stageOwnerReplacement(prepared.replacementId, {authority: nextAuthority, snapshot: ownerState.snapshot}), {committed: true})
1506
+ expect(restored.status().pid).toBe(legacyPid)
1507
+ expect(prepared.ownerState).toEqual(ownerState)
1508
+ expect(await upgraded.stageOwnerReplacement(prepared.replacementId, {authority: nextAuthority, snapshot: ownerState.snapshot})).toEqual({committed: true})
1502
1509
  await committed
1503
- assert.equal(restored.status().pid, legacyPid)
1510
+ expect(restored.status().pid).toBe(legacyPid)
1511
+
1512
+ const currentProcess = upgraded.process("release:v2:current-worker", {
1513
+ ...definition("current-worker"),
1514
+ lifecycle: {activateCommand: "sleep 0.05", activateTimeoutMs: 10, drainTimeoutMs: 0}
1515
+ })
1516
+ await currentProcess.start()
1517
+ await expect(currentProcess.activateStrict()).rejects.toThrow(/activate command timed out after 10ms/i)
1518
+ expect(restored.status().pid).toBe(legacyPid)
1504
1519
  await upgraded.shutdown()
1505
1520
  await upgraded.guardianExit()
1506
1521
  } finally {
@@ -1539,11 +1554,8 @@ test("split guardian rejects an owner-state update when its nested legacy defini
1539
1554
 
1540
1555
  await restored.recover()
1541
1556
  await legacyProcess.updateDefinition({...processDefinition, env: {REVISION: "external"}})
1542
- await assert.rejects(
1543
- () => restored.updateDefinition({...processDefinition, env: {REVISION: "candidate"}}, {authority: nextAuthority, snapshot: {...ownerState.snapshot, serviceReleaseIds: {service: "v2"}}}),
1544
- /provenance mismatch/
1545
- )
1546
- assert.deepEqual(await upgraded.ownerState(), committedOwnerState)
1557
+ await expect(restored.updateDefinition({...processDefinition, env: {REVISION: "candidate"}}, {authority: nextAuthority, snapshot: {...ownerState.snapshot, serviceReleaseIds: {service: "v2"}}})).rejects.toThrow(/provenance mismatch/)
1558
+ expect(await upgraded.ownerState()).toEqual(committedOwnerState)
1547
1559
  } finally {
1548
1560
  await legacyProcess.stop().catch(() => {})
1549
1561
  upgraded?.disconnect()
@@ -1596,14 +1608,15 @@ test("split guardian defers owner handoff until a nested legacy definition updat
1596
1608
  void claim.finally(() => { claimSettled = true })
1597
1609
  upgraded.disconnect()
1598
1610
  await new Promise((resolve) => setTimeout(resolve, 50))
1599
- assert.equal(claimSettled, false, "owner handoff must wait for the nested definition update")
1611
+ // Owner handoff must wait for the nested definition update.
1612
+ expect(claimSettled).toBe(false)
1600
1613
 
1601
1614
  await fs.writeFile(gatePath, "allow\n")
1602
1615
  await claim
1603
1616
  const updateError = await updateResult
1604
1617
 
1605
- assert.match(String(updateError), /connection closed while awaiting update/)
1606
- assert.deepEqual(await contender.ownerState(), {...committedOwnerState, listenerConnectionSources: {}})
1618
+ expect(String(updateError)).toMatch(/connection closed while awaiting update/)
1619
+ expect(await contender.ownerState()).toEqual({...committedOwnerState, listenerConnectionSources: {}})
1607
1620
  } finally {
1608
1621
  await fs.writeFile(gatePath, "allow\n").catch(() => {})
1609
1622
  await contender.shutdown().catch(() => {})
@@ -1642,7 +1655,8 @@ test("a disconnected legacy upgrade candidate does not strand its bridge guardia
1642
1655
  new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error("Legacy upgrade bridge guardian did not exit after candidate disconnect")), 1000) })
1643
1656
  ]).finally(() => { if (timeout) clearTimeout(timeout) })
1644
1657
  bridgeExited = true
1645
- assert.equal(fixture.child.exitCode, null, "the pre-split guardian must remain available after bridge abandonment")
1658
+ // The pre-split guardian must remain available after bridge abandonment.
1659
+ expect(fixture.child.exitCode).toBe(null)
1646
1660
  } finally {
1647
1661
  upgraded?.disconnect()
1648
1662
  if (!bridgeExited && upgraded?.pid) killExactProcessGroup(upgraded.pid)
@@ -1677,7 +1691,7 @@ test("a legacy bridge remains discoverable when its candidate disconnects after
1677
1691
  await upgraded.beginLegacyOwnerClaim(prepared.replacementId, 1000, statePath, recoverySnapshot)
1678
1692
  fixture.client.disconnect()
1679
1693
  await upgraded.completeLegacyOwnerClaim(prepared.replacementId)
1680
- assert.deepEqual(JSON.parse(await fs.readFile(statePath, "utf8")), recoverySnapshot)
1694
+ expect(JSON.parse(await fs.readFile(statePath, "utf8"))).toEqual(recoverySnapshot)
1681
1695
  upgraded.disconnect()
1682
1696
 
1683
1697
  replacement = new GuardianClient({...identity, pid: upgraded.pid})
@@ -1685,7 +1699,7 @@ test("a legacy bridge remains discoverable when its candidate disconnects after
1685
1699
  const resumed = await replacement.prepareOwnerReplacement(authority, nextAuthority)
1686
1700
  const committed = replacement.waitForEvent("replacement-committed")
1687
1701
 
1688
- assert.deepEqual(await replacement.stageOwnerReplacement(resumed.replacementId, {authority: nextAuthority, config: {statePath}, snapshot: ownerState.snapshot}), {committed: true})
1702
+ expect(await replacement.stageOwnerReplacement(resumed.replacementId, {authority: nextAuthority, config: {statePath}, snapshot: ownerState.snapshot})).toEqual({committed: true})
1689
1703
  await committed
1690
1704
  await replacement.shutdown()
1691
1705
  await upgraded.guardianExit()
@@ -1909,3 +1923,4 @@ function killExactProcessGroup(pid) {
1909
1923
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ESRCH") throw error
1910
1924
  }
1911
1925
  }
1926
+ })