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,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 GuardianClient from "../src/guardian-client.js"
14
13
 
14
+ describe("release-runtime-retention", () => {
15
+
15
16
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
16
17
  const dummyAppPath = path.join(repoRoot, "test", "fixtures", "dummy-app.js")
17
18
 
@@ -63,22 +64,22 @@ test("detached daemon survives deletion of the release-local Rollbridge installa
63
64
  const response = await fetch(`http://127.0.0.1:${proxyPort}/deferred-runtime`)
64
65
  const runtime = /** @type {{digest: string, format: number, path: string, version: string}} */ (status.daemonRuntime)
65
66
 
66
- assert.equal(status.activeReleaseId, "B")
67
- assert.equal(runtime.format, 1)
68
- assert.match(runtime.digest, /^[a-f0-9]{64}$/)
69
- assert.equal(path.dirname(runtime.path), runtimePath)
70
- assert.ok(!runtime.path.startsWith(releaseA), `runtime must be outside release A: ${runtime.path}`)
71
- assert.equal(response.status, 200)
72
- assert.equal(await response.text(), "deferred runtime loaded\n")
73
- assert.equal(typeof status.daemonPid, "number")
67
+ expect(status.activeReleaseId).toBe("B")
68
+ expect(runtime.format).toBe(1)
69
+ expect(runtime.digest).toMatch(/^[a-f0-9]{64}$/)
70
+ expect(path.dirname(runtime.path)).toBe(runtimePath)
71
+ expect({value: Boolean(!runtime.path.startsWith(releaseA)), context: `runtime must be outside release A: ${runtime.path}`}).toMatchObject({value: true})
72
+ expect(response.status).toBe(200)
73
+ expect(await response.text()).toBe("deferred runtime loaded\n")
74
+ expect(typeof status.daemonPid).toBe("number")
74
75
  const daemonPid = /** @type {number} */ (status.daemonPid)
75
76
 
76
77
  process.kill(daemonPid, "SIGKILL")
77
78
  const recoveredPid = await waitForChangedPid(pidPath, daemonPid)
78
79
  const recovered = await sendControlCommand({command: {command: "status"}, path: socketPath})
79
80
 
80
- assert.equal(recovered.daemonPid, recoveredPid)
81
- assert.equal(recovered.activeReleaseId, "B")
81
+ expect(recovered.daemonPid).toBe(recoveredPid)
82
+ expect(recovered.activeReleaseId).toBe("B")
82
83
  } finally {
83
84
  try {
84
85
  await sendControlCommand({command: {command: "shutdown"}, path: socketPath})
@@ -124,7 +125,7 @@ test("ensure-daemon resolves an explicit relative config before changing to its
124
125
  ])
125
126
  const status = await sendControlCommand({command: {command: "status"}, path: socketPath})
126
127
 
127
- assert.equal(status.activeReleaseId, "relative-config")
128
+ expect(status.activeReleaseId).toBe("relative-config")
128
129
  } finally {
129
130
  await sendControlCommand({command: {command: "shutdown"}, path: socketPath}).catch(() => undefined)
130
131
  await stopGuardian(statePath)
@@ -147,15 +148,12 @@ test("runtime preparation failure prevents daemon startup and deploy handoff", a
147
148
  await fs.writeFile(invalidRuntimePath, "occupied\n")
148
149
  await fs.writeFile(configPath, `export default ${JSON.stringify(basicConfig(socketPath), null, 2)}\n`)
149
150
 
150
- await assert.rejects(
151
- () => runReleaseCli(release, [
151
+ await expect(runReleaseCli(release, [
152
152
  "deploy", "--ensure-daemon", "--config", configPath,
153
153
  "--release-path", release, "--release-id", "blocked",
154
154
  "--daemon-runtime-path", invalidRuntimePath
155
- ]),
156
- /EEXIST|not a directory|ENOTDIR/
157
- )
158
- await assert.rejects(() => fs.stat(socketPath), {code: "ENOENT"})
155
+ ])).rejects.toThrow(/EEXIST|not a directory|ENOTDIR/)
156
+ await expect(fs.stat(socketPath)).rejects.toMatchObject({code: "ENOENT"})
159
157
  } finally {
160
158
  await fs.rm(root, {force: true, recursive: true})
161
159
  }
@@ -185,16 +183,13 @@ test("ensure-daemon refuses a responsive legacy daemon before deploy", async ()
185
183
  legacyDaemon.listen(socketPath, () => resolve(undefined))
186
184
  })
187
185
 
188
- await assert.rejects(
189
- () => runReleaseCli(release, [
186
+ await expect(runReleaseCli(release, [
190
187
  "deploy", "--ensure-daemon", "--config", configPath,
191
188
  "--release-path", release, "--release-id", "must-not-deploy",
192
189
  "--daemon-runtime-path", path.join(root, "runtime")
193
- ]),
194
- /legacy or mismatched runtime.*deploy was not sent/s
195
- )
190
+ ])).rejects.toThrow(/legacy or mismatched runtime.*deploy was not sent/s)
196
191
 
197
- assert.equal(deployReceived, false)
192
+ expect(deployReceived).toBe(false)
198
193
  } finally {
199
194
  await new Promise((resolve) => legacyDaemon.close(resolve))
200
195
  await fs.rm(root, {force: true, recursive: true})
@@ -230,15 +225,12 @@ test("ensure-daemon refuses a mismatched runtime attestation before deploy", asy
230
225
  daemon.listen(socketPath, () => resolve(undefined))
231
226
  })
232
227
 
233
- await assert.rejects(
234
- () => runReleaseCli(release, [
228
+ await expect(runReleaseCli(release, [
235
229
  "deploy", "--ensure-daemon", "--config", configPath,
236
230
  "--release-path", release, "--release-id", "must-not-deploy",
237
231
  "--daemon-runtime-path", path.join(root, "runtime")
238
- ]),
239
- /legacy or mismatched runtime.*deploy was not sent/s
240
- )
241
- assert.equal(deployReceived, false)
232
+ ])).rejects.toThrow(/legacy or mismatched runtime.*deploy was not sent/s)
233
+ expect(deployReceived).toBe(false)
242
234
  } finally {
243
235
  await new Promise((resolve) => daemon.close(resolve))
244
236
  await fs.rm(root, {force: true, recursive: true})
@@ -271,15 +263,14 @@ test("concurrent startup loser re-attests the winner before sending deploy", asy
271
263
  proxy: {drainTimeoutMs: 100, forceStopTimeoutMs: 100, host: "127.0.0.1", port: 0}
272
264
  }, null, 2)}\n`)
273
265
 
274
- const loserRejected = assert.rejects(
275
- runReleaseCli(loserRelease, [
266
+ const loserRejected = (async () => {
267
+ await expect(runReleaseCli(loserRelease, [
276
268
  "deploy", "--ensure-daemon", "--config", configPath,
277
269
  "--release-path", loserRelease, "--release-id", "loser",
278
270
  "--daemon-log-path", path.join(root, "loser.log"), "--daemon-pid-path", loserPidPath,
279
271
  "--daemon-runtime-path", path.join(root, "loser-runtime")
280
- ]),
281
- /legacy or mismatched runtime.*deploy was not sent/s
282
- )
272
+ ])).rejects.toThrow(/legacy or mismatched runtime.*deploy was not sent/s)
273
+ })()
283
274
 
284
275
  await waitForFile(loserPausedPath)
285
276
  await runReleaseCli(winnerRelease, [
@@ -292,7 +283,7 @@ test("concurrent startup loser re-attests the winner before sending deploy", asy
292
283
  await loserRejected
293
284
  const status = await sendControlCommand({command: {command: "status"}, path: socketPath})
294
285
 
295
- assert.equal(status.activeReleaseId, "winner")
286
+ expect(status.activeReleaseId).toBe("winner")
296
287
  } finally {
297
288
  try {
298
289
  await sendControlCommand({command: {command: "shutdown"}, path: socketPath})
@@ -343,7 +334,7 @@ async function prepareRelease(releasePath, deferredImport) {
343
334
  const marker = " proxyHttp(request, response) {\n"
344
335
  const deferredRoute = `${marker} if (request.url === "/deferred-runtime") {\n void import("./deferred-runtime.js")\n .then(({default: body}) => { response.writeHead(200); response.end(body) })\n .catch((error) => { response.writeHead(500); response.end(String(error)) })\n return\n }\n\n`
345
336
 
346
- assert.ok(source.includes(marker))
337
+ expect(source.includes(marker)).toBeTruthy()
347
338
  await fs.writeFile(daemonPath, source.replace(marker, deferredRoute))
348
339
  await fs.writeFile(path.join(packagePath, "src", "deferred-runtime.js"), "export default \"deferred runtime loaded\\n\"\n")
349
340
  }
@@ -360,7 +351,7 @@ async function installStartupPause(releasePath, pausedPath) {
360
351
  const marker = " const candidate = await startDaemonProcess({\n"
361
352
  const pause = ` await fsPromises.writeFile(${JSON.stringify(pausedPath)}, "paused\\n")\n await new Promise((resolve) => setTimeout(resolve, 750))\n\n${marker}`
362
353
 
363
- assert.ok(source.includes(marker))
354
+ expect(source.includes(marker)).toBeTruthy()
364
355
  await fs.writeFile(cliPath, source.replace(marker, pause))
365
356
  }
366
357
 
@@ -475,3 +466,4 @@ async function stopGuardian(statePath) {
475
466
  if (!error || typeof error !== "object" || !("code" in error) || !["ENOENT", "ESRCH"].includes(String(error.code))) throw error
476
467
  }
477
468
  }
469
+ })