rollbridge 0.1.39 → 0.1.40

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,106 @@ 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
411
501
  return undefined
412
502
  }
413
503
 
414
- await managed.activateStrict()
415
- assert.deepEqual(commands, [{command: "jobs activate", label: "activate command", pid: 4321, timeoutMs: 30000}])
504
+ await assert.rejects(() => managed.activateStrict(), /is not running for activation/)
505
+ assert.equal(hookRan, false)
506
+ })
507
+
508
+ test("quiesce waits for active-role restoration before retiring a restarted process", async () => {
509
+ const managed = buildLongLived(() => false)
510
+ const hooks = /** @type {string[]} */ ([])
511
+ /** @type {() => void} */
512
+ let allowActivation = () => {}
513
+ /** @type {() => void} */
514
+ let markActivationStarted = () => {}
515
+ const activationAllowed = new Promise((resolve) => { allowActivation = () => resolve(undefined) })
516
+ const activationStarted = new Promise((resolve) => { markActivationStarted = () => resolve(undefined) })
517
+
518
+ managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0, quietCommand: "jobs retire"}
519
+ managed.runHook = async (_command, _timeoutMs, label) => {
520
+ if (label === "activate command") {
521
+ hooks.push("activate:start")
522
+ markActivationStarted()
523
+ await activationAllowed
524
+ hooks.push("activate:end")
525
+ } else {
526
+ hooks.push("quiet")
527
+ }
528
+ return undefined
529
+ }
530
+ const start = assert.rejects(() => managed.start("crash", "active"), /quiesced before lifecycle role active was restored/)
416
531
 
417
- managed.runHook = async () => new Error("activation rejected")
418
- await assert.rejects(() => managed.activateStrict(), /activation rejected/)
532
+ try {
533
+ await activationStarted
534
+ const quiesce = managed.quiesceStrict()
535
+
536
+ await Promise.resolve()
537
+ assert.deepEqual(hooks, ["activate:start"], "retirement must not race ahead of role restoration")
538
+ allowActivation()
539
+ await Promise.all([start, quiesce])
540
+ assert.deepEqual(hooks, ["activate:start", "activate:end", "quiet"])
541
+ assert.equal(managed.status().state, "quiesced")
542
+ assert.equal(managed.lifecycleRole, "retired")
543
+ } finally {
544
+ allowActivation()
545
+ await start.catch(() => {})
546
+ await managed.stop()
547
+ }
419
548
  })
420
549
 
421
550
  test("sends the configured stopSignal as the graceful stop signal", async () => {