rollbridge 0.1.54 → 0.1.56

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 (36) hide show
  1. package/AGENTS.md +5 -0
  2. package/changelog.d/20260909120000-velocious-testing.md +1 -0
  3. package/changelog.d/20261008-stale-stopped-connection-count.md +14 -0
  4. package/eslint.config.js +8 -0
  5. package/package.json +3 -2
  6. package/src/daemon.js +30 -2
  7. package/src/release-group.js +21 -1
  8. package/test/completion.test.js +18 -16
  9. package/test/config-examples.test.js +16 -17
  10. package/test/config-path.test.js +10 -11
  11. package/test/config-validation.test.js +163 -167
  12. package/test/control-protocol.test.js +22 -21
  13. package/test/daemon-bootstrap.test.js +104 -104
  14. package/test/daemon-runtime.test.js +17 -26
  15. package/test/doctor.test.js +51 -49
  16. package/test/event-log.test.js +13 -11
  17. package/test/guardian-client.test.js +140 -148
  18. package/test/health.test.js +6 -4
  19. package/test/logs.test.js +17 -18
  20. package/test/managed-process.test.js +96 -91
  21. package/test/owner-recovery.test.js +226 -239
  22. package/test/owner-replacement.test.js +230 -222
  23. package/test/package-metadata.test.js +48 -39
  24. package/test/port-allocator.test.js +13 -16
  25. package/test/predeploy-cleanup.test.js +12 -10
  26. package/test/process-memory.test.js +17 -15
  27. package/test/proxy.test.js +10 -8
  28. package/test/recover.test.js +30 -23
  29. package/test/release-group.test.js +73 -17
  30. package/test/release-retention.test.js +10 -8
  31. package/test/release-runtime-retention.test.js +31 -39
  32. package/test/rollbridge.test.js +377 -396
  33. package/test/shutdown-completion.test.js +51 -51
  34. package/test/stale-connection-handoff.test.js +84 -0
  35. package/test/state-store.test.js +10 -8
  36. package/test/system-ids.test.js +15 -13
@@ -1,14 +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 {fileURLToPath} from "node:url"
9
8
  import RollbridgeDaemon, {releasesToPrune} from "../src/daemon.js"
10
9
  import {normalizeConfig} from "../src/config.js"
11
10
 
11
+ describe("release-retention", () => {
12
+
12
13
  const currentDir = path.dirname(fileURLToPath(import.meta.url))
13
14
  const dummyAppPath = path.join(currentDir, "fixtures", "dummy-app.js")
14
15
 
@@ -23,7 +24,7 @@ test("releasesToPrune keeps the most recent stopped releases and never active or
23
24
 
24
25
  const remove = releasesToPrune(releases, {keep: 1, maxAgeMs: 0}, Date.parse("2026-05-22T00:00:10.000Z"))
25
26
 
26
- assert.deepEqual([...remove].sort(), ["v1", "v2"])
27
+ expect([...remove].sort()).toEqual(["v1", "v2"])
27
28
  })
28
29
 
29
30
  test("releasesToPrune keeps the later-deployed release when stoppedAt ties", () => {
@@ -36,7 +37,7 @@ test("releasesToPrune keeps the later-deployed release when stoppedAt ties", ()
36
37
 
37
38
  const remove = releasesToPrune(releases, {keep: 1, maxAgeMs: 0}, Date.parse("2026-05-22T00:00:10.000Z"))
38
39
 
39
- assert.deepEqual(remove, ["v1"])
40
+ expect(remove).toEqual(["v1"])
40
41
  })
41
42
 
42
43
  test("releasesToPrune prunes stopped releases older than maxAgeMs", () => {
@@ -48,7 +49,7 @@ test("releasesToPrune prunes stopped releases older than maxAgeMs", () => {
48
49
 
49
50
  const remove = releasesToPrune(releases, {keep: 100, maxAgeMs: 30000}, now)
50
51
 
51
- assert.deepEqual(remove, ["old"])
52
+ expect(remove).toEqual(["old"])
52
53
  })
53
54
 
54
55
  test("the daemon prunes stopped releases beyond the retention count across deploys", async () => {
@@ -82,9 +83,9 @@ test("the daemon prunes stopped releases beyond the retention count across deplo
82
83
 
83
84
  const ids = daemon.status().releases.map((release) => release.releaseId)
84
85
 
85
- assert.ok(ids.includes("v3"), `active release should be retained, got ${JSON.stringify(ids)}`)
86
- assert.ok(!ids.includes("v1"), `oldest stopped release should be pruned, got ${JSON.stringify(ids)}`)
87
- assert.ok(ids.length <= 2, `expected at most the active release plus one stopped, got ${JSON.stringify(ids)}`)
86
+ expect({value: Boolean(ids.includes("v3")), context: `active release should be retained, got ${JSON.stringify(ids)}`}).toMatchObject({value: true})
87
+ expect({value: Boolean(!ids.includes("v1")), context: `oldest stopped release should be pruned, got ${JSON.stringify(ids)}`}).toMatchObject({value: true})
88
+ expect({value: Boolean(ids.length <= 2), context: `expected at most the active release plus one stopped, got ${JSON.stringify(ids)}`}).toMatchObject({value: true})
88
89
  } finally {
89
90
  await daemon.shutdown()
90
91
  await fs.rm(root, {force: true, recursive: true})
@@ -105,3 +106,4 @@ async function waitFor(callback) {
105
106
 
106
107
  throw new Error("Timed out waiting for condition")
107
108
  }
109
+ })
@@ -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
+ })