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,27 +1,36 @@
1
1
  // @ts-check
2
2
 
3
- import assert from "node:assert/strict"
4
3
  import fs from "node:fs/promises"
5
4
  import net from "node:net"
6
5
  import os from "node:os"
7
6
  import path from "node:path"
8
- import test, {after, before} from "node:test"
7
+ import {afterAll, beforeAll, describe, expect, test} from "@velocious/testing"
8
+ import {fileURLToPath} from "node:url"
9
9
  import RollbridgeDaemon from "../src/daemon.js"
10
10
  import {normalizeConfig} from "../src/config.js"
11
11
  import {sendControlCommand} from "../src/control-client.js"
12
12
 
13
+ describe("control-protocol", () => {
14
+
13
15
  let root = ""
14
16
  let socketPath = ""
15
17
  let daemon = /** @type {RollbridgeDaemon | undefined} */ (undefined)
18
+ const currentDir = path.dirname(fileURLToPath(import.meta.url))
19
+ const dummyAppPath = path.join(currentDir, "fixtures", "dummy-app.js")
20
+ const runningProcessCommand = `${JSON.stringify(process.execPath)} -e ${JSON.stringify("setInterval(() => {}, 1000)")}`
16
21
 
17
- before(async () => {
22
+ beforeAll(async () => {
18
23
  root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-control-"))
19
24
  socketPath = path.join(root, "rollbridge.sock")
20
25
 
21
26
  const config = normalizeConfig({
22
27
  application: "rollbridge-control-test",
23
28
  control: {path: socketPath},
24
- processes: [{command: "true", id: "web", policy: "proxied", port: {from: 0, to: 0}}],
29
+ processes: [
30
+ {command: runningProcessCommand, id: "beacon", policy: "service", port: {from: 0, to: 0}},
31
+ {command: `${JSON.stringify(process.execPath)} ${JSON.stringify(dummyAppPath)}`, health: {intervalMs: 50, path: "/ping", timeoutMs: 3000}, id: "web", policy: "proxied", port: {from: 0, to: 0}},
32
+ {command: runningProcessCommand, id: "jobs-main", policy: "singleton"}
33
+ ],
25
34
  proxy: {host: "127.0.0.1", port: 0}
26
35
  })
27
36
 
@@ -29,7 +38,7 @@ before(async () => {
29
38
  await daemon.start()
30
39
  })
31
40
 
32
- after(async () => {
41
+ afterAll(async () => {
33
42
  if (daemon) await daemon.shutdown()
34
43
  await fs.rm(root, {force: true, recursive: true})
35
44
  })
@@ -63,32 +72,84 @@ async function sendRawControlLine(rawLine) {
63
72
  test("malformed JSON returns an error response without crashing the daemon", async () => {
64
73
  const response = await sendRawControlLine("this is not json")
65
74
 
66
- assert.equal(response.status, "error")
67
- assert.match(String(response.error), /JSON/)
75
+ expect(response.status).toBe("error")
76
+ expect(String(response.error)).toMatch(/JSON/)
68
77
 
69
78
  // The daemon stays up and still answers valid commands afterwards.
70
79
  const status = await sendControlCommand({command: {command: "status"}, path: socketPath})
71
80
 
72
- assert.equal(status.application, "rollbridge-control-test")
81
+ expect(status.application).toBe("rollbridge-control-test")
73
82
  })
74
83
 
75
84
  test("non-object JSON is rejected as an invalid control command", async () => {
76
85
  const response = await sendRawControlLine("123")
77
86
 
78
- assert.equal(response.status, "error")
79
- assert.equal(response.error, "Control command must be an object")
87
+ expect(response.status).toBe("error")
88
+ expect(response.error).toBe("Control command must be an object")
80
89
  })
81
90
 
82
91
  test("an unknown control command returns a clear error", async () => {
83
92
  const response = await sendRawControlLine(JSON.stringify({command: "bogus"}))
84
93
 
85
- assert.equal(response.status, "error")
86
- assert.equal(response.error, "Unknown command: bogus")
94
+ expect(response.status).toBe("error")
95
+ expect(response.error).toBe("Unknown command: bogus")
87
96
  })
88
97
 
89
98
  test("a known command missing a required field returns a field error", async () => {
90
99
  const response = await sendRawControlLine(JSON.stringify({command: "deploy"}))
91
100
 
92
- assert.equal(response.status, "error")
93
- assert.equal(response.error, "releasePath is required")
101
+ expect(response.status).toBe("error")
102
+ expect(response.error).toBe("releasePath is required")
103
+ })
104
+ test("status can omit process logs without changing the default status payload", async () => {
105
+ if (!daemon) throw new Error("Missing required fixture: daemon")
106
+ await daemon.deploy({releaseId: "v1", releasePath: root, revision: "v1"})
107
+
108
+ const full = /** @type {import("../src/daemon.js").DaemonStatus} */ (await sendControlCommand({command: {command: "status"}, path: socketPath}))
109
+ const compact = /** @type {import("../src/daemon.js").DaemonStatusWithoutLogs} */ (await sendControlCommand({command: {command: "status", includeLogs: false}, path: socketPath}))
110
+ const invalid = await sendRawControlLine(JSON.stringify({command: "status", includeLogs: "false"}))
111
+
112
+ expect(full.releases[0]?.processes.every((processStatus) => Array.isArray(processStatus.logs))).toBeTruthy()
113
+ expect(full.services.every(({process: processStatus}) => Array.isArray(processStatus.logs))).toBeTruthy()
114
+ expect(full.singletons.every(({process: processStatus}) => Array.isArray(processStatus.logs))).toBeTruthy()
115
+ expect(statusWithoutProcessUptimes(compact)).toEqual(statusWithoutProcessUptimes({
116
+ ...full,
117
+ releases: full.releases.map((release) => ({
118
+ ...release,
119
+ processes: release.processes.map(({logs: _logs, ...processStatus}) => processStatus)
120
+ })),
121
+ services: full.services.map(({process, ...service}) => ({
122
+ ...service,
123
+ process: (({logs: _logs, ...processStatus}) => processStatus)(process)
124
+ })),
125
+ singletons: full.singletons.map(({process, ...singleton}) => ({
126
+ ...singleton,
127
+ process: (({logs: _logs, ...processStatus}) => processStatus)(process)
128
+ }))
129
+ }))
130
+ expect(invalid.status).toBe("error")
131
+ expect(invalid.error).toBe("includeLogs must be a boolean")
132
+ })
133
+
134
+ /**
135
+ * @param {import("../src/daemon.js").DaemonStatus | import("../src/daemon.js").DaemonStatusWithoutLogs} status - Status response.
136
+ * @returns {Record<string, import("../src/json.js").JsonValue>} Status without volatile process uptime.
137
+ */
138
+ function statusWithoutProcessUptimes(status) {
139
+ return {
140
+ ...status,
141
+ releases: status.releases.map((release) => ({
142
+ ...release,
143
+ processes: release.processes.map(({uptimeMs: _uptimeMs, ...processStatus}) => processStatus)
144
+ })),
145
+ services: status.services.map(({process, ...service}) => ({
146
+ ...service,
147
+ process: (({uptimeMs: _uptimeMs, ...processStatus}) => processStatus)(process)
148
+ })),
149
+ singletons: status.singletons.map(({process, ...singleton}) => ({
150
+ ...singleton,
151
+ process: (({uptimeMs: _uptimeMs, ...processStatus}) => processStatus)(process)
152
+ }))
153
+ }
154
+ }
94
155
  })
@@ -1,17 +1,18 @@
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 {sendControlCommand} from "../src/control-client.js"
13
12
  import {isProcessAlive, liveProcesses, readState, writeState} from "../src/state-store.js"
14
13
 
14
+ describe("daemon-bootstrap", () => {
15
+
15
16
  const currentDir = path.dirname(fileURLToPath(import.meta.url))
16
17
  const binPath = path.join(currentDir, "..", "bin", "rollbridge")
17
18
  const dummyAppPath = path.join(currentDir, "fixtures", "dummy-app.js")
@@ -21,43 +22,39 @@ const secondAttestation = `sha256:${"b".repeat(64)}`
21
22
 
22
23
  /** @typedef {{data?: Record<string, import("../src/json.js").JsonValue>, message?: string}} StructuredRecord */
23
24
 
24
- test("daemon bootstrap requires complete, safe, absolute inputs before binding listeners", async (t) => {
25
- const cases = [
26
- {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1"], message: /must be provided together/},
27
- {args: ["--config", "relative/config.js", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "abc123"], message: /--config must be an absolute path/},
28
- {args: ["--config", "CONFIG", "--release-path", "relative/release", "--release-id", "v1", "--revision", "abc123"], message: /--release-path must be an absolute path/},
29
- {args: ["--config", "CONFIG", "--release-path", "RELEASE_UNNORMALIZED", "--release-id", "v1", "--revision", "abc123"], message: /--release-path must be normalized/},
30
- {args: ["--config", "CONFIG", "--release-path", "RELEASE_MISSING", "--release-id", "v1", "--revision", "abc123"], message: /--release-path is not accessible/},
31
- {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "unsafe id", "--revision", "abc123"], message: /--release-id/},
32
- {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "unsafe revision"], message: /--revision/},
33
- {args: ["--config", "CONFIG", "--boot-attestation", firstAttestation], message: /accepted only with/},
34
- {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "abc123", "--boot-attestation", `sha256:${"A".repeat(64)}`], message: /--boot-attestation/},
35
- {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "abc123", "--boot-attestation", `sha512:${"a".repeat(64)}`], message: /--boot-attestation/},
36
- {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "abc123", "--boot-attestation", `sha256:${"a".repeat(63)}`], message: /--boot-attestation/}
37
- ]
38
-
39
- for (const testCase of cases) {
40
- await t.test(testCase.message.source, async () => {
41
- const fixture = await createFixture()
42
- const args = testCase.args.map((arg) => {
43
- if (arg === "CONFIG") return fixture.configPath
44
- if (arg === "RELEASE") return fixture.root
45
- if (arg === "RELEASE_UNNORMALIZED") return `${fixture.root}/child/..`
46
- if (arg === "RELEASE_MISSING") return path.join(fixture.root, "missing")
47
- return arg
48
- })
49
-
50
- try {
51
- const result = await runDaemon(args)
52
-
53
- assert.notEqual(result.code, 0)
54
- assert.match(result.stderr, testCase.message)
55
- await assert.rejects(() => fs.stat(fixture.socketPath), {code: "ENOENT"})
56
- await assert.rejects(() => fs.stat(fixture.startedPath), {code: "ENOENT"})
57
- } finally {
58
- await fs.rm(fixture.root, {force: true, recursive: true})
59
- }
60
- })
25
+ const invalidBootstrapCases = [
26
+ {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1"], message: /must be provided together/},
27
+ {args: ["--config", "relative/config.js", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "abc123"], message: /--config must be an absolute path/},
28
+ {args: ["--config", "CONFIG", "--release-path", "relative/release", "--release-id", "v1", "--revision", "abc123"], message: /--release-path must be an absolute path/},
29
+ {args: ["--config", "CONFIG", "--release-path", "RELEASE_UNNORMALIZED", "--release-id", "v1", "--revision", "abc123"], message: /--release-path must be normalized/},
30
+ {args: ["--config", "CONFIG", "--release-path", "RELEASE_MISSING", "--release-id", "v1", "--revision", "abc123"], message: /--release-path is not accessible/},
31
+ {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "unsafe id", "--revision", "abc123"], message: /--release-id/},
32
+ {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "unsafe revision"], message: /--revision/},
33
+ {args: ["--config", "CONFIG", "--boot-attestation", firstAttestation], message: /accepted only with/},
34
+ {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "abc123", "--boot-attestation", `sha256:${"A".repeat(64)}`], message: /--boot-attestation/},
35
+ {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "abc123", "--boot-attestation", `sha512:${"a".repeat(64)}`], message: /--boot-attestation/},
36
+ {args: ["--config", "CONFIG", "--release-path", "RELEASE", "--release-id", "v1", "--revision", "abc123", "--boot-attestation", `sha256:${"a".repeat(63)}`], message: /--boot-attestation/}
37
+ ]
38
+
39
+ test.each(invalidBootstrapCases)("daemon bootstrap rejects invalid startup arguments (%#)", async (/** @type {{args: string[], message: RegExp}} */ testCase) => {
40
+ const fixture = await createFixture()
41
+ const args = testCase.args.map((arg) => {
42
+ if (arg === "CONFIG") return fixture.configPath
43
+ if (arg === "RELEASE") return fixture.root
44
+ if (arg === "RELEASE_UNNORMALIZED") return `${fixture.root}/child/..`
45
+ if (arg === "RELEASE_MISSING") return path.join(fixture.root, "missing")
46
+ return arg
47
+ })
48
+
49
+ try {
50
+ const result = await runDaemon(args)
51
+
52
+ expect(result.code).not.toBe(0)
53
+ expect(result.stderr).toMatch(testCase.message)
54
+ await expect(fs.stat(fixture.socketPath)).rejects.toMatchObject({code: "ENOENT"})
55
+ await expect(fs.stat(fixture.startedPath)).rejects.toMatchObject({code: "ENOENT"})
56
+ } finally {
57
+ await fs.rm(fixture.root, {force: true, recursive: true})
61
58
  }
62
59
  })
63
60
 
@@ -70,19 +67,19 @@ test("daemon bootstrap activates the exact release through the foreground daemon
70
67
  const status = await sendControlCommand({command: {command: "status"}, path: fixture.socketPath})
71
68
  const activeRelease = assertRelease(status, "release-42")
72
69
 
73
- assert.equal(activeRelease.releasePath, fixture.root)
74
- assert.equal(activeRelease.revision, "abc123")
75
- assert.deepEqual(status.bootstrap, {
70
+ expect(activeRelease.releasePath).toBe(fixture.root)
71
+ expect(activeRelease.revision).toBe("abc123")
72
+ expect(status.bootstrap).toEqual({
76
73
  attestation: firstAttestation,
77
74
  releaseId: "release-42",
78
75
  releasePath: fixture.root,
79
76
  revision: "abc123"
80
77
  })
81
- assert.ok(status.proxy && typeof status.proxy === "object" && !Array.isArray(status.proxy) && typeof status.proxy.port === "number")
82
- assert.equal((await fetch(`http://127.0.0.1:${status.proxy.port}/release`).then((response) => response.text())).trim(), "release-42")
78
+ if (!(status.proxy && typeof status.proxy === "object" && !Array.isArray(status.proxy) && typeof status.proxy.port === "number")) throw new Error("Expected proxy status with a numeric port")
79
+ expect((await fetch(`http://127.0.0.1:${status.proxy.port}/release`).then((response) => response.text())).trim()).toBe("release-42")
83
80
 
84
81
  child.kill("SIGTERM")
85
- assert.equal((await once(child, "exit"))[0], 0)
82
+ expect((await once(child, "exit"))[0]).toBe(0)
86
83
  } finally {
87
84
  if (child.exitCode === null) child.kill("SIGKILL")
88
85
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -110,21 +107,23 @@ test("failed takeover bootstrap preserves the previously accepted owner", async
110
107
  "--takeover-owner"
111
108
  ])
112
109
 
113
- assert.notEqual(result.code, 0)
110
+ expect(result.code).not.toBe(0)
114
111
  const records = parseStructuredOutput(result.output)
115
112
  const failure = records.find((record) => record.message === "bootstrap activation failed")
116
113
  const candidatePid = Number(await fs.readFile(fixture.startedPath, "utf8"))
117
114
 
118
- assert.match(String(failure?.data?.error), /Health check failed/)
119
- assert.match(String(failure?.data?.stack), /Error: Health check failed/)
120
- assert.equal(isProcessAlive(candidatePid), false, "the failed candidate process must be stopped")
115
+ expect(String(failure?.data?.error)).toMatch(/Health check failed/)
116
+ expect(String(failure?.data?.stack)).toMatch(/Error: Health check failed/)
117
+ // The failed candidate process must be stopped.
118
+ expect(isProcessAlive(candidatePid)).toBe(false)
121
119
  const status = await sendControlCommand({command: {command: "status"}, path: fixture.socketPath})
122
- assert.equal(status.activeReleaseId, "accepted")
123
- assert.ok(status.bootstrap && typeof status.bootstrap === "object" && !Array.isArray(status.bootstrap))
124
- assert.equal(status.bootstrap.attestation, firstAttestation)
120
+ expect(status.activeReleaseId).toBe("accepted")
121
+ if (!(status.bootstrap && typeof status.bootstrap === "object" && !Array.isArray(status.bootstrap))) throw new Error("Expected bootstrap status")
122
+ expect(status.bootstrap.attestation).toBe(firstAttestation)
125
123
  const priorState = await readState(fixture.statePath)
126
124
 
127
- assert.ok(priorState && typeof priorState === "object" && !Array.isArray(priorState) && priorState.activeReleaseId === "accepted", "candidate cleanup must preserve the prior owner's state")
125
+ // Candidate cleanup must preserve the prior owner's state.
126
+ expect(priorState && typeof priorState === "object" && !Array.isArray(priorState) && priorState.activeReleaseId === "accepted").toBeTruthy()
128
127
  } finally {
129
128
  accepted.kill("SIGTERM")
130
129
  if (accepted.exitCode === null) await once(accepted, "exit")
@@ -139,20 +138,20 @@ test("daemon bootstrap does not expose control deploys until activation complete
139
138
 
140
139
  try {
141
140
  await started
142
- await assert.rejects(() => fs.stat(fixture.socketPath), {code: "ENOENT"})
141
+ await expect(fs.stat(fixture.socketPath)).rejects.toMatchObject({code: "ENOENT"})
143
142
 
144
143
  await fs.writeFile(fixture.healthGatePath, "ready\n")
145
144
  await waitForLog(child, "control socket listening")
146
145
 
147
146
  const status = await sendControlCommand({command: {command: "status"}, path: fixture.socketPath})
148
147
 
149
- assert.equal(status.activeReleaseId, "bootstrap-release")
150
- assert.ok(Array.isArray(status.releases))
151
- assert.equal(status.releases.length, 1)
148
+ expect(status.activeReleaseId).toBe("bootstrap-release")
149
+ if (!Array.isArray(status.releases)) throw new Error("Expected release statuses")
150
+ expect(status.releases.length).toBe(1)
152
151
  assertRelease(status, "bootstrap-release")
153
152
 
154
153
  child.kill("SIGTERM")
155
- assert.equal((await once(child, "exit"))[0], 0)
154
+ expect((await once(child, "exit"))[0]).toBe(0)
156
155
  } finally {
157
156
  if (child.exitCode === null) child.kill("SIGKILL")
158
157
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -167,12 +166,12 @@ test("plain daemon startup remains listener-only with no active release", async
167
166
  await waitForLog(child, "control socket listening")
168
167
  const status = await sendControlCommand({command: {command: "status"}, path: fixture.socketPath})
169
168
 
170
- assert.equal(status.activeReleaseId, null)
171
- assert.deepEqual(status.releases, [])
172
- assert.equal(status.bootstrap, undefined)
169
+ expect(status.activeReleaseId).toBe(null)
170
+ expect(status.releases).toEqual([])
171
+ expect(status.bootstrap).toBe(undefined)
173
172
 
174
173
  child.kill("SIGTERM")
175
- assert.equal((await once(child, "exit"))[0], 0)
174
+ expect((await once(child, "exit"))[0]).toBe(0)
176
175
  } finally {
177
176
  if (child.exitCode === null) child.kill("SIGKILL")
178
177
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -185,10 +184,10 @@ test("ensure-daemon rejects boot attestation instead of inheriting foreground id
185
184
  try {
186
185
  const result = await runRollbridge(["ensure-daemon", "--config", fixture.configPath, "--boot-attestation", firstAttestation])
187
186
 
188
- assert.notEqual(result.code, 0)
189
- assert.match(result.stderr, /unknown option '--boot-attestation'/)
190
- await assert.rejects(() => fs.stat(fixture.socketPath), {code: "ENOENT"})
191
- await assert.rejects(() => fs.stat(fixture.startedPath), {code: "ENOENT"})
187
+ expect(result.code).not.toBe(0)
188
+ expect(result.stderr).toMatch(/unknown option '--boot-attestation'/)
189
+ await expect(fs.stat(fixture.socketPath)).rejects.toMatchObject({code: "ENOENT"})
190
+ await expect(fs.stat(fixture.startedPath)).rejects.toMatchObject({code: "ENOENT"})
192
191
  } finally {
193
192
  await fs.rm(fixture.root, {force: true, recursive: true})
194
193
  }
@@ -206,15 +205,15 @@ test("otherwise identical foreground boots remain distinguishable by attestation
206
205
  await waitForLog(child, "control socket listening")
207
206
  const status = await sendControlCommand({command: {command: "status"}, path: fixture.socketPath})
208
207
 
209
- assert.equal(status.activeReleaseId, "same-release")
210
- assert.ok(status.bootstrap && typeof status.bootstrap === "object" && !Array.isArray(status.bootstrap))
208
+ expect(status.activeReleaseId).toBe("same-release")
209
+ if (!(status.bootstrap && typeof status.bootstrap === "object" && !Array.isArray(status.bootstrap))) throw new Error("Expected bootstrap status")
211
210
  attestations.push(status.bootstrap.attestation)
212
211
 
213
212
  child.kill("SIGTERM")
214
- assert.equal((await once(child, "exit"))[0], 0)
213
+ expect((await once(child, "exit"))[0]).toBe(0)
215
214
  }
216
215
 
217
- assert.deepEqual(attestations, [firstAttestation, secondAttestation])
216
+ expect(attestations).toEqual([firstAttestation, secondAttestation])
218
217
  } finally {
219
218
  await fs.rm(fixture.root, {force: true, recursive: true})
220
219
  }
@@ -231,10 +230,10 @@ test("SIGTERM during bootstrap activation follows the daemon shutdown path", asy
231
230
 
232
231
  const [code, signal] = await once(child, "exit")
233
232
 
234
- assert.equal(code, 0)
235
- assert.equal(signal, null)
236
- assert.equal(await fs.readFile(fixture.stoppedPath, "utf8"), String(managedPid))
237
- await assert.rejects(() => fs.stat(fixture.socketPath), {code: "ENOENT"})
233
+ expect(code).toBe(0)
234
+ expect(signal).toBe(null)
235
+ expect(await fs.readFile(fixture.stoppedPath, "utf8")).toBe(String(managedPid))
236
+ await expect(fs.stat(fixture.socketPath)).rejects.toMatchObject({code: "ENOENT"})
238
237
  } finally {
239
238
  if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL")
240
239
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -259,12 +258,12 @@ test("SIGTERM during multi-process bootstrap owns every process started after sh
259
258
  const records = output.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line))
260
259
  const recordedPids = new Set(records.filter((record) => record.message === "process started").map((record) => record.data?.pid))
261
260
 
262
- assert.equal(code, 0)
263
- assert.equal(signal, null)
264
- assert.notEqual(shutdownIndex, -1)
265
- assert.ok(startedAfterShutdown.length > 0, `fixture must start a later bootstrap process after triggering shutdown: ${JSON.stringify(events)}`)
266
- assert.deepEqual(startedAfterShutdown.filter((event) => !recordedPids.has(event.pid)), [])
267
- for (const event of startedAfterShutdown) assert.equal(isProcessAlive(event.pid), false, `expected process ${event.pid} to be stopped before daemon exit`)
261
+ expect(code).toBe(0)
262
+ expect(signal).toBe(null)
263
+ expect(shutdownIndex).not.toBe(-1)
264
+ expect({value: Boolean(startedAfterShutdown.length > 0), context: `fixture must start a later bootstrap process after triggering shutdown: ${JSON.stringify(events)}`}).toMatchObject({value: true})
265
+ expect(startedAfterShutdown.filter((event) => !recordedPids.has(event.pid))).toEqual([])
266
+ for (const event of startedAfterShutdown) expect({value: isProcessAlive(event.pid), context: `expected process ${event.pid} to be stopped before daemon exit`}).toMatchObject({value: false})
268
267
  } finally {
269
268
  if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL")
270
269
 
@@ -295,13 +294,13 @@ test("failed ordinary bootstrap completely shuts down attempt-owned resources an
295
294
  const records = parseStructuredOutput(result.output)
296
295
  const failure = records.find((record) => record.message === "bootstrap activation failed")
297
296
 
298
- assert.notEqual(result.code, 0)
299
- assert.equal(failure?.data?.releaseId, "ordinary-failure")
300
- assert.equal(failure?.data?.status, "error")
301
- assert.match(String(failure?.data?.error), /listen (?:EACCES|ENOENT)/)
302
- assert.match(String(failure?.data?.stack), /Error: listen (?:EACCES|ENOENT)/)
297
+ expect(result.code).not.toBe(0)
298
+ expect(failure?.data?.releaseId).toBe("ordinary-failure")
299
+ expect(failure?.data?.status).toBe("error")
300
+ expect(String(failure?.data?.error)).toMatch(/listen (?:EACCES|ENOENT)/)
301
+ expect(String(failure?.data?.stack)).toMatch(/Error: listen (?:EACCES|ENOENT)/)
303
302
  await assertAttemptResourcesStopped(fixture, records)
304
- await assert.rejects(() => fs.stat(fixture.socketPath), {code: "ENOENT"})
303
+ await expect(fs.stat(fixture.socketPath)).rejects.toMatchObject({code: "ENOENT"})
305
304
  } finally {
306
305
  await killAttemptProcesses(fixture.lifecyclePath)
307
306
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -323,13 +322,13 @@ test("failed takeover retirement completely shuts down and exits non-zero instea
323
322
  const records = parseStructuredOutput(result.output)
324
323
  const failure = records.find((record) => record.message === "bootstrap activation failed")
325
324
 
326
- assert.notEqual(result.code, 0)
327
- assert.equal(failure?.data?.releaseId, "orphaned-candidate")
328
- assert.equal(failure?.data?.status, "error")
329
- assert.match(String(failure?.data?.error), /connect ENOENT/)
330
- assert.match(String(failure?.data?.stack), /Error: connect ENOENT/)
325
+ expect(result.code).not.toBe(0)
326
+ expect(failure?.data?.releaseId).toBe("orphaned-candidate")
327
+ expect(failure?.data?.status).toBe("error")
328
+ expect(String(failure?.data?.error)).toMatch(/connect ENOENT/)
329
+ expect(String(failure?.data?.stack)).toMatch(/Error: connect ENOENT/)
331
330
  await assertAttemptResourcesStopped(fixture, records)
332
- await assert.rejects(() => fs.stat(fixture.socketPath), {code: "ENOENT"})
331
+ await expect(fs.stat(fixture.socketPath)).rejects.toMatchObject({code: "ENOENT"})
333
332
  } finally {
334
333
  await killAttemptProcesses(fixture.lifecyclePath)
335
334
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -354,13 +353,13 @@ test("daemon bootstrap reports but does not kill a live process from statePath",
354
353
  await waitForLog(child, "control socket listening")
355
354
  const status = await sendControlCommand({command: {command: "status"}, path: fixture.socketPath})
356
355
 
357
- assert.ok(leftover.pid !== undefined && isProcessAlive(leftover.pid))
358
- assert.deepEqual(status.orphans, [{id: "worker", pid: leftover.pid, releaseId: "previous"}])
356
+ expect(leftover.pid !== undefined && isProcessAlive(leftover.pid)).toBeTruthy()
357
+ expect(status.orphans).toEqual([{id: "worker", pid: leftover.pid, releaseId: "previous"}])
359
358
 
360
359
  child.kill("SIGTERM")
361
360
  await once(child, "exit")
362
361
 
363
- assert.deepEqual(liveProcesses(await readState(fixture.statePath)), [{id: "worker", pid: leftover.pid, releaseId: "previous"}])
362
+ expect(liveProcesses(await readState(fixture.statePath))).toEqual([{id: "worker", pid: leftover.pid, releaseId: "previous"}])
364
363
  } finally {
365
364
  if (child.exitCode === null) child.kill("SIGKILL")
366
365
  leftover.kill("SIGKILL")
@@ -388,8 +387,8 @@ test("failed daemon bootstrap preserves prior live process records in statePath"
388
387
  "--revision", "bad123"
389
388
  ])
390
389
 
391
- assert.notEqual(result.code, 0)
392
- assert.deepEqual(liveProcesses(await readState(fixture.statePath)), [{id: "worker", pid: leftover.pid, releaseId: "previous"}])
390
+ expect(result.code).not.toBe(0)
391
+ expect(liveProcesses(await readState(fixture.statePath))).toEqual([{id: "worker", pid: leftover.pid, releaseId: "previous"}])
393
392
  } finally {
394
393
  leftover.kill("SIGKILL")
395
394
  await fs.rm(fixture.root, {force: true, recursive: true})
@@ -444,7 +443,7 @@ async function createFixture({attemptOwnedProcesses = false, fixedPorts = false,
444
443
  const fifo = spawn("mkfifo", [gatePath])
445
444
  const [code] = await once(fifo, "exit")
446
445
 
447
- assert.equal(code, 0)
446
+ expect(code).toBe(0)
448
447
  }
449
448
 
450
449
  await fs.writeFile(configPath, `${setup}module.exports = ${JSON.stringify(config, null, 2)}\n`)
@@ -578,11 +577,11 @@ async function assertAttemptResourcesStopped(fixture, records) {
578
577
  const managedStarts = new Set(records.filter((record) => record.message === "process started").map((record) => record.data?.processId))
579
578
  const managedExits = new Set(records.filter((record) => record.message === "process exited").map((record) => record.data?.processId))
580
579
 
581
- assert.deepEqual(managedStarts, expectedProcessIds)
582
- assert.deepEqual(managedExits, expectedProcessIds)
580
+ expect(managedStarts).toEqual(expectedProcessIds)
581
+ expect(managedExits).toEqual(expectedProcessIds)
583
582
  for (const event of started) {
584
- assert.equal(stoppedPids.has(event.pid), true, `${event.processId} must receive graceful shutdown`)
585
- assert.equal(isProcessAlive(Number(event.pid)), false, `${event.processId} pid ${event.pid} must be gone before daemon exit`)
583
+ expect({value: stoppedPids.has(event.pid), context: `${event.processId} must receive graceful shutdown`}).toMatchObject({value: true})
584
+ expect({value: isProcessAlive(Number(event.pid)), context: `${event.processId} pid ${event.pid} must be gone before daemon exit`}).toMatchObject({value: false})
586
585
  }
587
586
 
588
587
  await assertPortAvailable(fixture.processPort)
@@ -674,9 +673,10 @@ async function waitForLog(child, message) {
674
673
  * @returns {Record<string, import("../src/json.js").JsonValue>} Matching release status.
675
674
  */
676
675
  function assertRelease(status, releaseId) {
677
- assert.ok(Array.isArray(status.releases))
676
+ if (!Array.isArray(status.releases)) throw new Error("Expected release statuses")
678
677
  const release = status.releases.find((candidate) => candidate && typeof candidate === "object" && "releaseId" in candidate && candidate.releaseId === releaseId)
679
678
 
680
- assert.ok(release && typeof release === "object" && !Array.isArray(release))
679
+ if (!(release && typeof release === "object" && !Array.isArray(release))) throw new Error(`Release ${releaseId} should be present`)
681
680
  return release
682
681
  }
682
+ })
@@ -1,12 +1,15 @@
1
1
  // @ts-check
2
2
 
3
- import assert from "node:assert/strict"
4
3
  import fs from "node:fs/promises"
5
4
  import os from "node:os"
6
5
  import path from "node:path"
7
- import test from "node:test"
6
+ import {describe, expect, test} from "@velocious/testing"
8
7
  import {prepareDaemonRuntime} from "../src/daemon-runtime.js"
9
8
 
9
+ describe("daemon-runtime", () => {
10
+
11
+ const posixPermissionsTest = process.platform === "win32" ? test.skip : test
12
+
10
13
  test("concurrent runtime preparation converges on one validated content-addressed snapshot", async () => {
11
14
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-runtime-concurrent-"))
12
15
 
@@ -17,13 +20,13 @@ test("concurrent runtime preparation converges on one validated content-addresse
17
20
  prepareDaemonRuntime(root)
18
21
  ])
19
22
 
20
- assert.deepEqual(identities, [identities[0], identities[0], identities[0]])
21
- assert.match(identities[0].digest, /^[a-f0-9]{64}$/)
22
- assert.equal(path.dirname(identities[0].path), root)
23
- assert.equal(JSON.parse(await fs.readFile(path.join(identities[0].path, "runtime.json"), "utf8")).digest, identities[0].digest)
23
+ expect(identities).toEqual([identities[0], identities[0], identities[0]])
24
+ expect(identities[0].digest).toMatch(/^[a-f0-9]{64}$/)
25
+ expect(path.dirname(identities[0].path)).toBe(root)
26
+ expect(JSON.parse(await fs.readFile(path.join(identities[0].path, "runtime.json"), "utf8")).digest).toBe(identities[0].digest)
24
27
 
25
28
  const entries = (await fs.readdir(root)).filter((entry) => entry.startsWith(".prepare-"))
26
- assert.deepEqual(entries, [])
29
+ expect(entries).toEqual([])
27
30
  } finally {
28
31
  await fs.rm(root, {force: true, recursive: true})
29
32
  }
@@ -36,13 +39,13 @@ test("preparation fails closed when an existing content-addressed snapshot is co
36
39
  const identity = await prepareDaemonRuntime(root)
37
40
 
38
41
  await fs.writeFile(path.join(identity.path, "src", "daemon.js"), "corrupt\n")
39
- await assert.rejects(() => prepareDaemonRuntime(root), /runtime validation failed/)
42
+ await expect(prepareDaemonRuntime(root)).rejects.toThrow(/runtime validation failed/)
40
43
  } finally {
41
44
  await fs.rm(root, {force: true, recursive: true})
42
45
  }
43
46
  })
44
47
 
45
- test("runtime preparation rejects a symlinked or shared-writable parent", async (t) => {
48
+ posixPermissionsTest("runtime preparation rejects a symlinked or shared-writable parent", async () => {
46
49
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-runtime-permissions-"))
47
50
  const target = path.join(root, "target")
48
51
  const symlink = path.join(root, "symlink")
@@ -51,27 +54,17 @@ test("runtime preparation rejects a symlinked or shared-writable parent", async
51
54
  try {
52
55
  await fs.mkdir(target)
53
56
  await fs.symlink(target, symlink, "dir")
54
- await assert.rejects(() => prepareDaemonRuntime(symlink), /must be a real directory/)
55
-
56
- if (process.platform === "win32") {
57
- t.skip("POSIX directory permissions are not available on Windows")
58
- return
59
- }
57
+ await expect(prepareDaemonRuntime(symlink)).rejects.toThrow(/must be a real directory/)
60
58
 
61
59
  await fs.mkdir(shared, {mode: 0o777})
62
60
  await fs.chmod(shared, 0o777)
63
- await assert.rejects(() => prepareDaemonRuntime(shared), /must not be writable by group or other users/)
61
+ await expect(prepareDaemonRuntime(shared)).rejects.toThrow(/must not be writable by group or other users/)
64
62
  } finally {
65
63
  await fs.rm(root, {force: true, recursive: true})
66
64
  }
67
65
  })
68
66
 
69
- test("runtime preparation rejects a private leaf beneath a replaceable ancestor", async (t) => {
70
- if (process.platform === "win32") {
71
- t.skip("POSIX directory permissions are not available on Windows")
72
- return
73
- }
74
-
67
+ posixPermissionsTest("runtime preparation rejects a private leaf beneath a replaceable ancestor", async () => {
75
68
  const unsafeAncestor = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-runtime-unsafe-ancestor-"))
76
69
  const privateLeaf = path.join(unsafeAncestor, "private-runtime")
77
70
 
@@ -79,12 +72,10 @@ test("runtime preparation rejects a private leaf beneath a replaceable ancestor"
79
72
  await fs.chmod(unsafeAncestor, 0o777)
80
73
  await fs.mkdir(privateLeaf, {mode: 0o700})
81
74
 
82
- await assert.rejects(
83
- () => prepareDaemonRuntime(privateLeaf),
84
- /ancestor must be sticky or not writable by group or other users/
85
- )
75
+ await expect(prepareDaemonRuntime(privateLeaf)).rejects.toThrow(/ancestor must be sticky or not writable by group or other users/)
86
76
  } finally {
87
77
  await fs.chmod(unsafeAncestor, 0o700)
88
78
  await fs.rm(unsafeAncestor, {force: true, recursive: true})
89
79
  }
90
80
  })
81
+ })