switchroom 0.14.93 → 0.15.1

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.
@@ -436,3 +436,269 @@ describe("evaluateFleetAllExhausted", () => {
436
436
  if (d.kind === "notify") expect(d.message).toContain("Reset time unknown");
437
437
  });
438
438
  });
439
+
440
+ // ── hardening: tuning resolver + claim keys (2026-06-09 incident) ───────────
441
+
442
+ import {
443
+ resolveQuotaWatchTuning,
444
+ buildQuotaClaimKey,
445
+ DEFAULT_QUOTA_WATCH_MAX_STALE_MS,
446
+ DEFAULT_QUOTA_WATCH_LATE_RECOVERY_MS,
447
+ QUOTA_WATCH_CLAIM_WINDOW_MS,
448
+ type QuotaWatchDecision,
449
+ } from "../quota-watch.js";
450
+
451
+ describe("resolveQuotaWatchTuning", () => {
452
+ it("defaults: stale gate 60min, late-recovery 6h, dedup on, probe-fail send off", () => {
453
+ const t = resolveQuotaWatchTuning({});
454
+ expect(t.maxStaleMs).toBe(DEFAULT_QUOTA_WATCH_MAX_STALE_MS);
455
+ expect(t.lateRecoveryMs).toBe(DEFAULT_QUOTA_WATCH_LATE_RECOVERY_MS);
456
+ expect(t.fleetDedup).toBe(true);
457
+ expect(t.sendOnProbeFail).toBe(false);
458
+ });
459
+
460
+ it("each knob is individually kill-switchable", () => {
461
+ const t = resolveQuotaWatchTuning({
462
+ SWITCHROOM_QUOTA_WATCH_MAX_STALE_MS: "0",
463
+ SWITCHROOM_QUOTA_WATCH_LATE_RECOVERY_MS: "0",
464
+ SWITCHROOM_QUOTA_WATCH_FLEET_DEDUP: "0",
465
+ SWITCHROOM_QUOTA_WATCH_SEND_ON_PROBE_FAIL: "1",
466
+ });
467
+ expect(t.maxStaleMs).toBe(0);
468
+ expect(t.lateRecoveryMs).toBe(0);
469
+ expect(t.fleetDedup).toBe(false);
470
+ expect(t.sendOnProbeFail).toBe(true);
471
+ });
472
+
473
+ it("garbage numeric values fall back to defaults", () => {
474
+ const t = resolveQuotaWatchTuning({
475
+ SWITCHROOM_QUOTA_WATCH_MAX_STALE_MS: "banana",
476
+ SWITCHROOM_QUOTA_WATCH_LATE_RECOVERY_MS: "-5",
477
+ });
478
+ expect(t.maxStaleMs).toBe(DEFAULT_QUOTA_WATCH_MAX_STALE_MS);
479
+ expect(t.lateRecoveryMs).toBe(DEFAULT_QUOTA_WATCH_LATE_RECOVERY_MS);
480
+ });
481
+
482
+ it("claim window exceeds one poll cycle (15 min) so all agents' observations of one edge dedup", () => {
483
+ expect(QUOTA_WATCH_CLAIM_WINDOW_MS).toBeGreaterThan(15 * 60_000);
484
+ });
485
+ });
486
+
487
+ describe("buildQuotaClaimKey", () => {
488
+ it("is namespaced and distinct per account, transition, and chat", () => {
489
+ const k1 = buildQuotaClaimKey("a@x.com", "entered-throttling", 111);
490
+ const k2 = buildQuotaClaimKey("a@x.com", "entered-throttling", 222);
491
+ const k3 = buildQuotaClaimKey("a@x.com", "recovered-to-healthy", 111);
492
+ const k4 = buildQuotaClaimKey("b@x.com", "entered-throttling", 111);
493
+ expect(new Set([k1, k2, k3, k4]).size).toBe(4);
494
+ expect(k1).toBe("quota-watch:a@x.com:entered-throttling:111");
495
+ });
496
+ });
497
+
498
+ // ── hardening: staleness gate ────────────────────────────────────────────────
499
+
500
+ const TUNING = {
501
+ maxStaleMs: DEFAULT_QUOTA_WATCH_MAX_STALE_MS,
502
+ lateRecoveryMs: DEFAULT_QUOTA_WATCH_LATE_RECOVERY_MS,
503
+ };
504
+
505
+ describe("evaluateQuotaWatchAccount — staleness gate", () => {
506
+ it("a cached snapshot past maxStaleMs carries no opinion (skip, even on a real edge)", () => {
507
+ const staleHealthy: AccountSnapshot = {
508
+ ...HEALTHY_SNAP,
509
+ capturedAtMs: NOW - DEFAULT_QUOTA_WATCH_MAX_STALE_MS - 1,
510
+ };
511
+ const d = evaluateQuotaWatchAccount({
512
+ agentName: "test", snap: staleHealthy, prev: PREV_WAS_THROTTLING, now: NOW, tuning: TUNING,
513
+ });
514
+ expect(d.kind).toBe("skip");
515
+ expect((d as { reason: string }).reason).toBe("stale-snapshot");
516
+ });
517
+
518
+ it("a cached snapshot inside the shelf life classifies normally", () => {
519
+ const freshEnough: AccountSnapshot = {
520
+ ...THROTTLING_5H,
521
+ capturedAtMs: NOW - 5 * 60_000,
522
+ };
523
+ const d = evaluateQuotaWatchAccount({
524
+ agentName: "test", snap: freshEnough, prev: PREV_WAS_HEALTHY, now: NOW, tuning: TUNING,
525
+ });
526
+ expect(d.kind).toBe("notify");
527
+ });
528
+
529
+ it("live-probe snapshots (no capturedAtMs) are never stale-gated", () => {
530
+ const d = evaluateQuotaWatchAccount({
531
+ agentName: "test", snap: THROTTLING_5H, prev: PREV_WAS_HEALTHY, now: NOW, tuning: TUNING,
532
+ });
533
+ expect(d.kind).toBe("notify");
534
+ });
535
+
536
+ it("maxStaleMs=0 disables the gate (legacy behaviour)", () => {
537
+ const ancient: AccountSnapshot = { ...THROTTLING_5H, capturedAtMs: NOW - 86_400_000 };
538
+ const d = evaluateQuotaWatchAccount({
539
+ agentName: "test", snap: ancient, prev: PREV_WAS_HEALTHY, now: NOW,
540
+ tuning: { maxStaleMs: 0, lateRecoveryMs: 0 },
541
+ });
542
+ expect(d.kind).toBe("notify");
543
+ });
544
+ });
545
+
546
+ // ── hardening: boot-tick + late-recovery reconciliation ─────────────────────
547
+
548
+ describe("evaluateQuotaWatchAccount — recovery reconciliation", () => {
549
+ it("recovery on a boot tick reconciles silently (the fleet-bounce flood case)", () => {
550
+ const d = evaluateQuotaWatchAccount({
551
+ agentName: "test", snap: HEALTHY_SNAP, prev: PREV_WAS_THROTTLING, now: NOW,
552
+ bootTick: true, tuning: TUNING,
553
+ });
554
+ expect(d.kind).toBe("reconcile");
555
+ const r = d as Extract<QuotaWatchDecision, { kind: "reconcile" }>;
556
+ expect(r.reason).toBe("boot-tick-recovery");
557
+ expect(r.transition).toBe("recovered-to-healthy");
558
+ // The latch must clear so the edge never re-fires.
559
+ expect(r.newAccountState.lastNotifiedHealth).toBe("healthy");
560
+ });
561
+
562
+ it("recovery whose 🟡 warning is older than lateRecoveryMs reconciles silently", () => {
563
+ const prev = {
564
+ lastNotifiedHealth: "throttling" as const,
565
+ lastNotifiedAt: NOW - DEFAULT_QUOTA_WATCH_LATE_RECOVERY_MS - 1,
566
+ };
567
+ const d = evaluateQuotaWatchAccount({
568
+ agentName: "test", snap: HEALTHY_SNAP, prev, now: NOW, tuning: TUNING,
569
+ });
570
+ expect(d.kind).toBe("reconcile");
571
+ expect((d as Extract<QuotaWatchDecision, { kind: "reconcile" }>).reason).toBe("late-recovery");
572
+ });
573
+
574
+ it("a prompt recovery on a normal tick still notifies", () => {
575
+ const prev = { lastNotifiedHealth: "throttling" as const, lastNotifiedAt: NOW - 30 * 60_000 };
576
+ const d = evaluateQuotaWatchAccount({
577
+ agentName: "test", snap: HEALTHY_SNAP, prev, now: NOW, tuning: TUNING,
578
+ });
579
+ expect(d.kind).toBe("notify");
580
+ expect((d as Extract<QuotaWatchDecision, { kind: "notify" }>).transition).toBe("recovered-to-healthy");
581
+ });
582
+
583
+ it("a 🟡 warning still notifies on a boot tick (warnings are level-state news)", () => {
584
+ const d = evaluateQuotaWatchAccount({
585
+ agentName: "test", snap: THROTTLING_5H, prev: PREV_WAS_HEALTHY, now: NOW,
586
+ bootTick: true, tuning: TUNING,
587
+ });
588
+ expect(d.kind).toBe("notify");
589
+ expect((d as Extract<QuotaWatchDecision, { kind: "notify" }>).transition).toBe("entered-throttling");
590
+ });
591
+
592
+ it("lateRecoveryMs=0 disables reconciliation (legacy always-send)", () => {
593
+ const prev = { lastNotifiedHealth: "throttling" as const, lastNotifiedAt: NOW - 86_400_000 };
594
+ const d = evaluateQuotaWatchAccount({
595
+ agentName: "test", snap: HEALTHY_SNAP, prev, now: NOW,
596
+ tuning: { maxStaleMs: 0, lateRecoveryMs: 0 },
597
+ });
598
+ expect(d.kind).toBe("notify");
599
+ });
600
+
601
+ it("omitting bootTick/tuning entirely preserves the pre-hardening table", () => {
602
+ const prev = { lastNotifiedHealth: "throttling" as const, lastNotifiedAt: NOW - 86_400_000 };
603
+ const d = evaluateQuotaWatchAccount({
604
+ agentName: "test", snap: HEALTHY_SNAP, prev, now: NOW,
605
+ });
606
+ expect(d.kind).toBe("notify");
607
+ });
608
+ });
609
+
610
+ // ── total enumeration: every (state × input) cell of the decision FSM ────────
611
+ //
612
+ // Operator standard (feedback_prove_finite_fsm_not_sample): the decision
613
+ // function is a finite table — prove it by enumerating EVERY cell, not by
614
+ // sampling. Dimensions:
615
+ // prevHealth ∈ {null, healthy, throttling}
616
+ // currentHealth ∈ {healthy, throttling, blocked, unknown}
617
+ // staleness ∈ {live (no capturedAtMs), fresh-cache, stale-cache}
618
+ // bootTick ∈ {false, true}
619
+ // warningAge ∈ {recent (< lateRecoveryMs), late (> lateRecoveryMs)}
620
+ // = 3 × 4 × 3 × 2 × 2 = 144 cells. The expected kind for every cell is
621
+ // computed from the documented table independently and asserted.
622
+
623
+ describe("evaluateQuotaWatchAccount — total enumeration (144 cells)", () => {
624
+ const PREVS = [null, "healthy", "throttling"] as const;
625
+ const CURRENTS = ["healthy", "throttling", "blocked", "unknown"] as const;
626
+ const STALENESS = ["live", "fresh-cache", "stale-cache"] as const;
627
+ const BOOLS = [false, true] as const;
628
+ const AGES = ["recent", "late"] as const;
629
+
630
+ const SNAPS: Record<(typeof CURRENTS)[number], QuotaUtilization | null> = {
631
+ healthy: makeQuota(30, 40),
632
+ throttling: makeQuota(85, 40),
633
+ blocked: makeQuota(99.9, 99.9),
634
+ unknown: null,
635
+ };
636
+
637
+ it("every cell matches the documented table", () => {
638
+ let cells = 0;
639
+ for (const prevHealth of PREVS) {
640
+ for (const currentHealth of CURRENTS) {
641
+ for (const staleness of STALENESS) {
642
+ for (const bootTick of BOOLS) {
643
+ for (const age of AGES) {
644
+ cells++;
645
+ const lastNotifiedAt =
646
+ prevHealth === null
647
+ ? 0
648
+ : age === "late"
649
+ ? NOW - DEFAULT_QUOTA_WATCH_LATE_RECOVERY_MS - 1
650
+ : NOW - 30 * 60_000;
651
+ const prev = { lastNotifiedHealth: prevHealth, lastNotifiedAt };
652
+ const snap: AccountSnapshot = {
653
+ label: "enum@example.com",
654
+ isActive: false,
655
+ quota: SNAPS[currentHealth],
656
+ capturedAtMs:
657
+ staleness === "live"
658
+ ? undefined
659
+ : staleness === "fresh-cache"
660
+ ? NOW - 60_000
661
+ : NOW - DEFAULT_QUOTA_WATCH_MAX_STALE_MS - 1,
662
+ };
663
+ const d = evaluateQuotaWatchAccount({
664
+ agentName: "enum", snap, prev, now: NOW, bootTick, tuning: TUNING,
665
+ });
666
+
667
+ // Independent expectation, straight from the documented table.
668
+ let expected: "notify" | "reconcile" | "skip";
669
+ const effPrev = prevHealth ?? "healthy";
670
+ if (staleness === "stale-cache") {
671
+ expected = "skip"; // gate fires before everything else
672
+ } else if (currentHealth === "blocked" || currentHealth === "unknown") {
673
+ expected = "skip"; // credits-watch domain / no data
674
+ } else if (currentHealth === effPrev) {
675
+ expected = "skip"; // steady-state
676
+ } else if (currentHealth === "throttling") {
677
+ expected = "notify"; // warnings always notify
678
+ } else {
679
+ // recovery: throttling → healthy
680
+ expected = bootTick || age === "late" ? "reconcile" : "notify";
681
+ }
682
+
683
+ expect(
684
+ d.kind,
685
+ `cell prev=${prevHealth} cur=${currentHealth} stale=${staleness} boot=${bootTick} age=${age}`,
686
+ ).toBe(expected);
687
+
688
+ // Terminal-state invariant: any notify/reconcile lands the
689
+ // latch on currentHealth, so re-running the SAME cell with the
690
+ // updated state is always a skip (the FSM cannot loop).
691
+ if (d.kind === "notify" || d.kind === "reconcile") {
692
+ const again = evaluateQuotaWatchAccount({
693
+ agentName: "enum", snap, prev: d.newAccountState, now: NOW, bootTick, tuning: TUNING,
694
+ });
695
+ expect(again.kind, `re-run of cell prev=${prevHealth} cur=${currentHealth}`).toBe("skip");
696
+ }
697
+ }
698
+ }
699
+ }
700
+ }
701
+ }
702
+ expect(cells).toBe(144);
703
+ });
704
+ });