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,19 +1,20 @@
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 RollbridgeDaemon from "../src/daemon.js"
12
11
  import {normalizeConfig} from "../src/config.js"
13
12
  import {runEnvironmentChecks, runReleaseChecks} from "../src/doctor.js"
14
13
  import {writeState} from "../src/state-store.js"
15
14
  import {runCli} from "../src/cli.js"
16
15
 
16
+ describe("doctor", () => {
17
+
17
18
  /**
18
19
  * @param {object} args - Options.
19
20
  * @param {string} args.controlPath - Control socket path.
@@ -72,7 +73,7 @@ async function occupyPort() {
72
73
  function checkNamed(checks, name) {
73
74
  const check = checks.find((candidate) => candidate.name === name)
74
75
 
75
- assert.ok(check, `expected a "${name}" check`)
76
+ if (!check) throw new Error(`expected a "${name}" check`)
76
77
 
77
78
  return check
78
79
  }
@@ -83,9 +84,9 @@ test("runEnvironmentChecks passes when no daemon runs, the port is free, and the
83
84
  try {
84
85
  const checks = await runEnvironmentChecks(buildConfig({controlPath: path.join(root, "rollbridge.sock"), proxyPort: await freePort()}))
85
86
 
86
- assert.equal(checkNamed(checks, "control socket").ok, true)
87
- assert.equal(checkNamed(checks, "control socket directory").ok, true)
88
- assert.equal(checkNamed(checks, "proxy port").ok, true)
87
+ expect(checkNamed(checks, "control socket").ok).toBe(true)
88
+ expect(checkNamed(checks, "control socket directory").ok).toBe(true)
89
+ expect(checkNamed(checks, "proxy port").ok).toBe(true)
89
90
  } finally {
90
91
  await fs.rm(root, {force: true, recursive: true})
91
92
  }
@@ -99,8 +100,8 @@ test("runEnvironmentChecks reports an unavailable proxy port", async () => {
99
100
  const checks = await runEnvironmentChecks(buildConfig({controlPath: path.join(root, "rollbridge.sock"), proxyPort: port}))
100
101
  const proxyCheck = checkNamed(checks, "proxy port")
101
102
 
102
- assert.equal(proxyCheck.ok, false)
103
- assert.match(proxyCheck.detail, /unavailable/)
103
+ expect(proxyCheck.ok).toBe(false)
104
+ expect(proxyCheck.detail).toMatch(/unavailable/)
104
105
  } finally {
105
106
  await new Promise((resolve) => server.close(() => resolve(undefined)))
106
107
  await fs.rm(root, {force: true, recursive: true})
@@ -113,8 +114,8 @@ test("runEnvironmentChecks checks the state path directory and reports no orphan
113
114
  try {
114
115
  const checks = await runEnvironmentChecks(buildConfig({controlPath: path.join(root, "rollbridge.sock"), proxyPort: await freePort(), statePath: path.join(root, "state.json")}))
115
116
 
116
- assert.equal(checkNamed(checks, "state path directory").ok, true)
117
- assert.equal(checkNamed(checks, "orphaned processes").ok, true)
117
+ expect(checkNamed(checks, "state path directory").ok).toBe(true)
118
+ expect(checkNamed(checks, "orphaned processes").ok).toBe(true)
118
119
  } finally {
119
120
  await fs.rm(root, {force: true, recursive: true})
120
121
  }
@@ -126,8 +127,8 @@ test("runEnvironmentChecks omits state checks when no statePath is configured",
126
127
  try {
127
128
  const checks = await runEnvironmentChecks(buildConfig({controlPath: path.join(root, "rollbridge.sock"), proxyPort: await freePort()}))
128
129
 
129
- assert.ok(!checks.some((check) => check.name === "state path directory"))
130
- assert.ok(!checks.some((check) => check.name === "orphaned processes"))
130
+ expect(!checks.some((check) => check.name === "state path directory")).toBeTruthy()
131
+ expect(!checks.some((check) => check.name === "orphaned processes")).toBeTruthy()
131
132
  } finally {
132
133
  await fs.rm(root, {force: true, recursive: true})
133
134
  }
@@ -146,8 +147,8 @@ test("runEnvironmentChecks does not flag orphans while a daemon is running", asy
146
147
  const orphanCheck = checkNamed(checks, "orphaned processes")
147
148
 
148
149
  // A running daemon's persisted pids are its own managed processes, not orphans.
149
- assert.equal(orphanCheck.ok, true)
150
- assert.match(orphanCheck.detail, /a daemon is running/)
150
+ expect(orphanCheck.ok).toBe(true)
151
+ expect(orphanCheck.detail).toMatch(/a daemon is running/)
151
152
  } finally {
152
153
  await daemon.shutdown()
153
154
  await fs.rm(root, {force: true, recursive: true})
@@ -172,8 +173,8 @@ test("runEnvironmentChecks reports orphaned processes left in the state file", a
172
173
  const checks = await runEnvironmentChecks(buildConfig({controlPath: path.join(root, "rollbridge.sock"), proxyPort: await freePort(), statePath}))
173
174
  const orphanCheck = checkNamed(checks, "orphaned processes")
174
175
 
175
- assert.equal(orphanCheck.ok, false)
176
- assert.match(orphanCheck.detail, new RegExp(`worker \\(pid ${leftover.pid}\\)`))
176
+ expect(orphanCheck.ok).toBe(false)
177
+ expect(orphanCheck.detail).toMatch(new RegExp(`worker \\(pid ${leftover.pid}\\)`))
177
178
  } finally {
178
179
  leftover.kill("SIGKILL")
179
180
  await fs.rm(root, {force: true, recursive: true})
@@ -184,8 +185,8 @@ test("runEnvironmentChecks reports a missing control socket directory", async ()
184
185
  const checks = await runEnvironmentChecks(buildConfig({controlPath: "/rollbridge-doctor-missing-dir/rollbridge.sock", proxyPort: await freePort()}))
185
186
  const directoryCheck = checkNamed(checks, "control socket directory")
186
187
 
187
- assert.equal(directoryCheck.ok, false)
188
- assert.match(directoryCheck.detail, /missing or not writable/)
188
+ expect(directoryCheck.ok).toBe(false)
189
+ expect(directoryCheck.detail).toMatch(/missing or not writable/)
189
190
  })
190
191
 
191
192
  test("runEnvironmentChecks passes when the running Rollbridge daemon holds the socket and port", async () => {
@@ -199,9 +200,9 @@ test("runEnvironmentChecks passes when the running Rollbridge daemon holds the s
199
200
  const checks = await runEnvironmentChecks(config)
200
201
  const socketCheck = checkNamed(checks, "control socket")
201
202
 
202
- assert.equal(socketCheck.ok, true)
203
- assert.match(socketCheck.detail, /Rollbridge daemon for "doctor-test" is running/)
204
- assert.equal(checkNamed(checks, "proxy port").ok, true)
203
+ expect(socketCheck.ok).toBe(true)
204
+ expect(socketCheck.detail).toMatch(/Rollbridge daemon for "doctor-test" is running/)
205
+ expect(checkNamed(checks, "proxy port").ok).toBe(true)
205
206
  } finally {
206
207
  await daemon.shutdown()
207
208
  await fs.rm(root, {force: true, recursive: true})
@@ -222,8 +223,8 @@ test("runEnvironmentChecks fails when the running daemon does not own the config
222
223
  try {
223
224
  const checks = await runEnvironmentChecks(changedConfig)
224
225
 
225
- assert.equal(checkNamed(checks, "control socket").ok, true)
226
- assert.equal(checkNamed(checks, "proxy port").ok, false)
226
+ expect(checkNamed(checks, "control socket").ok).toBe(true)
227
+ expect(checkNamed(checks, "proxy port").ok).toBe(false)
227
228
  } finally {
228
229
  await new Promise((resolve) => server.close(() => resolve(undefined)))
229
230
  await daemon.shutdown()
@@ -256,9 +257,9 @@ test("runReleaseChecks passes for an existing release with resolvable templates"
256
257
  const config = releaseConfig({processes: [{command: "run web --port {{port}} --release {{releaseId}}", cwd: "{{releasePath}}/backend", id: "web", policy: "proxied", port: {from: 18000, to: 18099}}], root})
257
258
  const checks = await runReleaseChecks(config, {releasePath})
258
259
 
259
- assert.equal(checkNamed(checks, "release path").ok, true)
260
- assert.equal(checkNamed(checks, "process templates").ok, true)
261
- assert.equal(checkNamed(checks, "process working directories").ok, true)
260
+ expect(checkNamed(checks, "release path").ok).toBe(true)
261
+ expect(checkNamed(checks, "process templates").ok).toBe(true)
262
+ expect(checkNamed(checks, "process working directories").ok).toBe(true)
262
263
  } finally {
263
264
  await fs.rm(root, {force: true, recursive: true})
264
265
  }
@@ -274,8 +275,8 @@ test("runReleaseChecks flags a command that references an undefined template var
274
275
  const config = releaseConfig({processes: [{command: "run web --secret {{missingVar}}", id: "web", policy: "proxied", port: {from: 18000, to: 18099}}], root})
275
276
  const templates = checkNamed(await runReleaseChecks(config, {releasePath}), "process templates")
276
277
 
277
- assert.equal(templates.ok, false)
278
- assert.match(templates.detail, /web:.*missingVar/)
278
+ expect(templates.ok).toBe(false)
279
+ expect(templates.detail).toMatch(/web:.*missingVar/)
279
280
  } finally {
280
281
  await fs.rm(root, {force: true, recursive: true})
281
282
  }
@@ -292,12 +293,12 @@ test("runReleaseChecks reports a missing process working directory", async () =>
292
293
  const config = releaseConfig({processes: [{command: "run web", cwd: "{{releasePath}}/backend", id: "web", policy: "proxied", port: {from: 18000, to: 18099}}], root})
293
294
  const checks = await runReleaseChecks(config, {releasePath})
294
295
 
295
- assert.equal(checkNamed(checks, "release path").ok, true)
296
+ expect(checkNamed(checks, "release path").ok).toBe(true)
296
297
 
297
298
  const directories = checkNamed(checks, "process working directories")
298
299
 
299
- assert.equal(directories.ok, false)
300
- assert.match(directories.detail, /web .*backend/)
300
+ expect(directories.ok).toBe(false)
301
+ expect(directories.detail).toMatch(/web .*backend/)
301
302
  } finally {
302
303
  await fs.rm(root, {force: true, recursive: true})
303
304
  }
@@ -310,7 +311,7 @@ test("runReleaseChecks reports a missing release path", async () => {
310
311
  const config = releaseConfig({processes: [{command: "run web", id: "web", policy: "proxied", port: {from: 18000, to: 18099}}], root})
311
312
  const checks = await runReleaseChecks(config, {releasePath: path.join(root, "does-not-exist")})
312
313
 
313
- assert.equal(checkNamed(checks, "release path").ok, false)
314
+ expect(checkNamed(checks, "release path").ok).toBe(false)
314
315
  } finally {
315
316
  await fs.rm(root, {force: true, recursive: true})
316
317
  }
@@ -355,8 +356,8 @@ test("doctor CLI passes for a valid, bindable config", async () => {
355
356
  try {
356
357
  const output = await captureCli(["node", "rollbridge", "doctor", "-c", path.join(root, "rollbridge.js")])
357
358
 
358
- assert.match(output, /All checks passed\./)
359
- assert.notEqual(process.exitCode, 1)
359
+ expect(output).toMatch(/All checks passed\./)
360
+ expect(process.exitCode).not.toBe(1)
360
361
  } finally {
361
362
  await fs.rm(root, {force: true, recursive: true})
362
363
  }
@@ -370,8 +371,8 @@ test("doctor CLI fails and exits non-zero for an invalid config", async () => {
370
371
  try {
371
372
  const output = await captureCli(["node", "rollbridge", "doctor", "-c", path.join(root, "rollbridge.js")])
372
373
 
373
- assert.equal(process.exitCode, 1)
374
- assert.match(output, /✗ config:/)
374
+ expect(process.exitCode).toBe(1)
375
+ expect(output).toMatch(/✗ config:/)
375
376
  } finally {
376
377
  process.exitCode = 0
377
378
  await fs.rm(root, {force: true, recursive: true})
@@ -396,15 +397,15 @@ test("doctor --json emits structured checks", async () => {
396
397
  try {
397
398
  const passing = JSON.parse(await captureCli(["node", "rollbridge", "doctor", "--json", "-c", path.join(okRoot, "rollbridge.js")]))
398
399
 
399
- assert.equal(passing.ok, true)
400
- assert.ok(passing.checks.some((/** @type {{name: string, ok: boolean}} */ check) => check.name === "proxy port" && check.ok === true))
401
- assert.notEqual(process.exitCode, 1)
400
+ expect(passing.ok).toBe(true)
401
+ expect(passing.checks.some((/** @type {{name: string, ok: boolean}} */ check) => check.name === "proxy port" && check.ok === true)).toBeTruthy()
402
+ expect(process.exitCode).not.toBe(1)
402
403
 
403
404
  const failing = JSON.parse(await captureCli(["node", "rollbridge", "doctor", "--json", "-c", path.join(badRoot, "rollbridge.js")]))
404
405
 
405
- assert.equal(failing.ok, false)
406
- assert.ok(failing.checks.some((/** @type {{name: string, ok: boolean}} */ check) => check.name === "config" && check.ok === false))
407
- assert.equal(process.exitCode, 1)
406
+ expect(failing.ok).toBe(false)
407
+ expect(failing.checks.some((/** @type {{name: string, ok: boolean}} */ check) => check.name === "config" && check.ok === false)).toBeTruthy()
408
+ expect(process.exitCode).toBe(1)
408
409
  } finally {
409
410
  process.exitCode = 0
410
411
  await fs.rm(okRoot, {force: true, recursive: true})
@@ -430,11 +431,11 @@ test("doctor --release-path adds release checks, passing or failing on the worki
430
431
  try {
431
432
  const passing = await captureCli(["node", "rollbridge", "doctor", "-c", path.join(root, "rollbridge.js"), "--release-path", releasePath])
432
433
 
433
- assert.match(passing, /✓ release path:/)
434
- assert.match(passing, /✓ process templates:/)
435
- assert.match(passing, /✓ process working directories:/)
436
- assert.match(passing, /All checks passed\./)
437
- assert.notEqual(process.exitCode, 1)
434
+ expect(passing).toMatch(/✓ release path:/)
435
+ expect(passing).toMatch(/✓ process templates:/)
436
+ expect(passing).toMatch(/✓ process working directories:/)
437
+ expect(passing).toMatch(/All checks passed\./)
438
+ expect(process.exitCode).not.toBe(1)
438
439
 
439
440
  // A release without the rendered backend directory fails the working-directories check.
440
441
  const emptyRelease = path.join(root, "empty-release")
@@ -443,10 +444,11 @@ test("doctor --release-path adds release checks, passing or failing on the worki
443
444
 
444
445
  const failing = await captureCli(["node", "rollbridge", "doctor", "-c", path.join(root, "rollbridge.js"), "--release-path", emptyRelease])
445
446
 
446
- assert.match(failing, /✗ process working directories:/)
447
- assert.equal(process.exitCode, 1)
447
+ expect(failing).toMatch(/✗ process working directories:/)
448
+ expect(process.exitCode).toBe(1)
448
449
  } finally {
449
450
  process.exitCode = 0
450
451
  await fs.rm(root, {force: true, recursive: true})
451
452
  }
452
453
  })
454
+ })
@@ -1,9 +1,10 @@
1
1
  // @ts-check
2
2
 
3
- import assert from "node:assert/strict"
4
- import test from "node:test"
3
+ import {describe, expect, test} from "@velocious/testing"
5
4
  import EventLog from "../src/event-log.js"
6
5
 
6
+ describe("event-log", () => {
7
+
7
8
  test("records events with a timestamp, message, and data", () => {
8
9
  const log = new EventLog(10)
9
10
 
@@ -11,9 +12,9 @@ test("records events with a timestamp, message, and data", () => {
11
12
 
12
13
  const [event] = log.recent()
13
14
 
14
- assert.equal(event.message, "traffic switched")
15
- assert.deepEqual(event.data, {releaseId: "v1"})
16
- assert.match(event.at, /^\d{4}-\d{2}-\d{2}T.*Z$/)
15
+ expect(event.message).toBe("traffic switched")
16
+ expect(event.data).toEqual({releaseId: "v1"})
17
+ expect(event.at).toMatch(/^\d{4}-\d{2}-\d{2}T.*Z$/)
17
18
  })
18
19
 
19
20
  test("drops the oldest events once the limit is exceeded", () => {
@@ -23,8 +24,8 @@ test("drops the oldest events once the limit is exceeded", () => {
23
24
 
24
25
  const events = log.recent()
25
26
 
26
- assert.equal(events.length, 3)
27
- assert.deepEqual(events.map((event) => event.data.index), [2, 3, 4])
27
+ expect(events.length).toBe(3)
28
+ expect(events.map((event) => event.data.index)).toEqual([2, 3, 4])
28
29
  })
29
30
 
30
31
  test("recent(limit) returns only the most recent events, oldest first", () => {
@@ -32,7 +33,7 @@ test("recent(limit) returns only the most recent events, oldest first", () => {
32
33
 
33
34
  for (let index = 0; index < 5; index += 1) log.record("tick", {index})
34
35
 
35
- assert.deepEqual(log.recent(2).map((event) => event.data.index), [3, 4])
36
+ expect(log.recent(2).map((event) => event.data.index)).toEqual([3, 4])
36
37
  })
37
38
 
38
39
  test("recent returns every event when the limit is omitted or not a positive number", () => {
@@ -40,7 +41,8 @@ test("recent returns every event when the limit is omitted or not a positive num
40
41
 
41
42
  for (let index = 0; index < 3; index += 1) log.record("tick", {index})
42
43
 
43
- assert.equal(log.recent().length, 3)
44
- assert.equal(log.recent(0).length, 3)
45
- assert.equal(log.recent(99).length, 3)
44
+ expect(log.recent().length).toBe(3)
45
+ expect(log.recent(0).length).toBe(3)
46
+ expect(log.recent(99).length).toBe(3)
47
+ })
46
48
  })