rollbridge 0.1.39 → 0.1.41

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.
@@ -66,6 +66,26 @@ test("keeps every output line when fewer than the retention limit are produced",
66
66
  assert.deepEqual(logs.map((entry) => entry.line), ["one", "two"])
67
67
  })
68
68
 
69
+ test("reassembles output lines split across stream chunks", () => {
70
+ const managed = buildProcess(50)
71
+
72
+ managed.appendLog("stdout", "8191:x")
73
+ assert.deepEqual(managed.status().logs, [])
74
+
75
+ managed.appendLog("stdout", "xx\n")
76
+ assert.deepEqual(managed.status().logs.map((entry) => entry.line), ["8191:xxx"])
77
+ })
78
+
79
+ test("bounds an output fragment that never terminates", () => {
80
+ const managed = buildProcess(50)
81
+ const fragment = "x".repeat(1024 * 1024)
82
+
83
+ managed.appendLog("stdout", fragment)
84
+
85
+ assert.equal(managed.status().logs.length, 15)
86
+ assert.equal(managed.outputBuffers.stdout.length, 64 * 1024)
87
+ })
88
+
69
89
  test("emits each output line after retaining it", () => {
70
90
  const managed = buildProcess(50)
71
91
  let observed
@@ -198,6 +218,27 @@ test("records the manual start reason", async () => {
198
218
  }
199
219
  })
200
220
 
221
+ test("a later stop cancels a start queued behind an in-flight stop", async () => {
222
+ const managed = buildProcess(50)
223
+ let finishStop = () => {}
224
+
225
+ managed.command = `${JSON.stringify(process.execPath)} -e ${JSON.stringify("setInterval(() => {}, 1000)")}`
226
+ managed.stopPromise = new Promise((resolve) => {
227
+ finishStop = resolve
228
+ })
229
+ const queuedStart = managed.start()
230
+ const finalStop = managed.stop()
231
+
232
+ finishStop()
233
+ await Promise.all([queuedStart, finalStop])
234
+ try {
235
+ assert.equal(managed.status().pid, undefined)
236
+ assert.equal(managed.status().state, "stopped")
237
+ } finally {
238
+ await managed.stop()
239
+ }
240
+ })
241
+
201
242
  test("does not record a start reason when the spawn fails", async () => {
202
243
  const managed = new ManagedProcess({
203
244
  command: "true",
@@ -404,18 +445,133 @@ test("activateStrict runs the configured activation command once per call and re
404
445
  const commands = /** @type {{command: string, label: string, pid: number | undefined, timeoutMs: number}[]} */ ([])
405
446
  const managed = buildLongLived(() => false)
406
447
 
448
+ try {
449
+ await managed.start()
450
+ const pid = managed.pid
451
+
452
+ assert.ok(pid)
453
+ managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0}
454
+ managed.runHook = async (command, timeoutMs, label, hookPid) => {
455
+ commands.push({command, label, pid: hookPid, timeoutMs})
456
+ return undefined
457
+ }
458
+
459
+ await managed.activateStrict()
460
+ assert.deepEqual(commands, [{command: "jobs activate", label: "activate command", pid, timeoutMs: 30000}])
461
+
462
+ managed.runHook = async () => new Error("activation rejected")
463
+ await assert.rejects(() => managed.activateStrict(), /activation rejected/)
464
+ } finally {
465
+ await managed.stop()
466
+ }
467
+ })
468
+
469
+ test("activateStrict rejects when the activated process is replaced while its hook runs", async () => {
470
+ const managed = buildLongLived(() => false)
471
+
472
+ await managed.start()
473
+ const child = managed.child
474
+ const pid = managed.pid
475
+
476
+ try {
477
+ managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0}
478
+ managed.lifecycleRole = "candidate"
479
+ managed.runHook = async () => {
480
+ managed.child = undefined
481
+ managed.pid = undefined
482
+ return undefined
483
+ }
484
+
485
+ await assert.rejects(() => managed.activateStrict(), /exited before activation completed/)
486
+ assert.equal(managed.lifecycleRole, "candidate")
487
+ } finally {
488
+ managed.child = child
489
+ managed.pid = pid
490
+ await managed.stop()
491
+ }
492
+ })
493
+
494
+ test("activateStrict rejects an activation request when its process is not running", async () => {
495
+ const managed = buildLongLived(() => false)
496
+ let hookRan = false
497
+
407
498
  managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0}
408
- managed.pid = 4321
409
- managed.runHook = async (command, timeoutMs, label, pid) => {
410
- commands.push({command, label, pid, timeoutMs})
499
+ managed.runHook = async () => {
500
+ hookRan = true
501
+ return undefined
502
+ }
503
+
504
+ await assert.rejects(() => managed.activateStrict(), /is not running for activation/)
505
+ assert.equal(hookRan, false)
506
+ })
507
+
508
+ test("reactivateStrict restores a retained quiesced process only after activation succeeds", async () => {
509
+ const managed = buildLongLived(() => false)
510
+ const hooks = /** @type {string[]} */ ([])
511
+
512
+ managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0, quietCommand: "jobs retire"}
513
+ managed.runHook = async (_command, _timeoutMs, label) => {
514
+ hooks.push(label)
515
+ if (label === "activate command" && hooks.length === 2) return new Error("restoration rejected")
411
516
  return undefined
412
517
  }
413
518
 
414
- await managed.activateStrict()
415
- assert.deepEqual(commands, [{command: "jobs activate", label: "activate command", pid: 4321, timeoutMs: 30000}])
519
+ try {
520
+ await managed.start()
521
+ await managed.quiesceStrict()
522
+ await assert.rejects(() => managed.reactivateStrict(), /restoration rejected/)
523
+ assert.equal(managed.status().state, "quiesced")
524
+ assert.equal(managed.status().lifecycleRole, "retired")
525
+
526
+ await managed.reactivateStrict()
527
+ assert.equal(managed.status().state, "running")
528
+ assert.equal(managed.status().lifecycleRole, "active")
529
+ assert.deepEqual(hooks, ["quiet command", "activate command", "activate command"])
530
+ } finally {
531
+ await managed.stop()
532
+ }
533
+ })
416
534
 
417
- managed.runHook = async () => new Error("activation rejected")
418
- await assert.rejects(() => managed.activateStrict(), /activation rejected/)
535
+ test("quiesce waits for active-role restoration before retiring a restarted process", async () => {
536
+ const managed = buildLongLived(() => false)
537
+ const hooks = /** @type {string[]} */ ([])
538
+ /** @type {() => void} */
539
+ let allowActivation = () => {}
540
+ /** @type {() => void} */
541
+ let markActivationStarted = () => {}
542
+ const activationAllowed = new Promise((resolve) => { allowActivation = () => resolve(undefined) })
543
+ const activationStarted = new Promise((resolve) => { markActivationStarted = () => resolve(undefined) })
544
+
545
+ managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0, quietCommand: "jobs retire"}
546
+ managed.runHook = async (_command, _timeoutMs, label) => {
547
+ if (label === "activate command") {
548
+ hooks.push("activate:start")
549
+ markActivationStarted()
550
+ await activationAllowed
551
+ hooks.push("activate:end")
552
+ } else {
553
+ hooks.push("quiet")
554
+ }
555
+ return undefined
556
+ }
557
+ const start = assert.rejects(() => managed.start("crash", "active"), /quiesced before lifecycle role active was restored/)
558
+
559
+ try {
560
+ await activationStarted
561
+ const quiesce = managed.quiesceStrict()
562
+
563
+ await Promise.resolve()
564
+ assert.deepEqual(hooks, ["activate:start"], "retirement must not race ahead of role restoration")
565
+ allowActivation()
566
+ await Promise.all([start, quiesce])
567
+ assert.deepEqual(hooks, ["activate:start", "activate:end", "quiet"])
568
+ assert.equal(managed.status().state, "quiesced")
569
+ assert.equal(managed.lifecycleRole, "retired")
570
+ } finally {
571
+ allowActivation()
572
+ await start.catch(() => {})
573
+ await managed.stop()
574
+ }
419
575
  })
420
576
 
421
577
  test("sends the configured stopSignal as the graceful stop signal", async () => {