import { describe, expect, test } from 'bun:test'; import { type EscalatableAlert, type EscalationInput, type EscalationStep, type RouteForEscalation, decideEscalation, meetsSeverityFloor, } from './escalation'; const START = new Date('2026-07-28T03:00:00Z'); const at = (minutes: number) => new Date(START.getTime() + minutes * 60_000); /** peter-signal at 0m, wife-signal at 10m, peter-email at 30m. */ const STEPS: EscalationStep[] = [ { stepIndex: 0, routeId: 'peter-signal', delayMinutes: 0 }, { stepIndex: 1, routeId: 'wife-signal', delayMinutes: 10 }, { stepIndex: 2, routeId: 'peter-email', delayMinutes: 30 }, ]; const ROUTES = new Map([ ['peter-signal', { id: 'peter-signal', severityFloor: 'warning', enabled: true }], ['wife-signal', { id: 'wife-signal', severityFloor: 'critical', enabled: true }], ['peter-email', { id: 'peter-email', severityFloor: 'warning', enabled: true }], ]); function alert(over: Partial = {}): EscalatableAlert { return { id: 'a1', severity: 'critical', escalationStep: 0, acked: false, resolved: false, suppressed: false, awaitingConfirmation: false, silencedUntil: null, graceUntil: START, escalationStartedAt: START, ...over, }; } const decide = (over: Partial = {}) => decideEscalation({ alert: alert(), steps: STEPS, routes: ROUTES, now: START, ...over }); describe('meetsSeverityFloor', () => { test('critical clears a warning floor', () => { expect(meetsSeverityFloor('critical', 'warning')).toBe(true); }); test('warning does not clear a critical floor', () => { expect(meetsSeverityFloor('warning', 'critical')).toBe(false); }); test('equal severities clear', () => { expect(meetsSeverityFloor('critical', 'critical')).toBe(true); }); }); describe('the happy path', () => { test('step 0 notifies the primary immediately', () => { expect(decide()).toEqual({ type: 'notify', routeId: 'peter-signal', stepIndex: 0, nextStepDueAt: at(10), }); }); test('step 1 is not due before its delay elapses', () => { expect(decide({ alert: alert({ escalationStep: 1 }), now: at(9) })).toEqual({ type: 'skip', reason: 'not_yet_due', }); }); test('unacked at 10m escalates to the secondary', () => { expect(decide({ alert: alert({ escalationStep: 1 }), now: at(10) })).toEqual({ type: 'notify', routeId: 'wife-signal', stepIndex: 1, nextStepDueAt: at(30), }); }); test('the final step has no following step', () => { const decision = decide({ alert: alert({ escalationStep: 2 }), now: at(30) }); expect(decision).toEqual({ type: 'notify', routeId: 'peter-email', stepIndex: 2, nextStepDueAt: null, }); }); test('past the last step the policy is exhausted', () => { expect(decide({ alert: alert({ escalationStep: 3 }), now: at(60) })).toEqual({ type: 'skip', reason: 'policy_exhausted', }); }); }); describe('reasons to stay silent', () => { // The alert self-resolved at minute four. Nobody further should hear about it. test.each([ ['resolved', { resolved: true }], ['acked', { acked: true }], ['suppressed', { suppressed: true }], ['awaiting_confirmation', { awaitingConfirmation: true }], ] as const)('skips when %s', (reason, over) => { expect(decide({ alert: alert({ escalationStep: 1, ...over }), now: at(10) })).toEqual({ type: 'skip', reason, }); }); test('skips while silenced, and resumes once the silence expires', () => { const silenced = alert({ escalationStep: 1, silencedUntil: at(20) }); expect(decide({ alert: silenced, now: at(10) })).toEqual({ type: 'skip', reason: 'silenced' }); expect(decide({ alert: silenced, now: at(21) })).toMatchObject({ type: 'notify' }); }); // A check that fails once and passes 30s later must never page. test('skips inside the grace window', () => { const pending = alert({ graceUntil: at(1) }); expect(decide({ alert: pending, now: START })).toEqual({ type: 'skip', reason: 'within_grace', }); }); test('notifies once the grace window elapses', () => { const pending = alert({ graceUntil: at(1) }); expect(decide({ alert: pending, now: at(2) })).toMatchObject({ type: 'notify' }); }); }); describe('severity floors', () => { // wife-signal takes critical only. A warning must not burn ten minutes of // silence waiting to skip her — it should fall through to the next route. test('a filtered route is passed over immediately, not waited on', () => { const warn = alert({ severity: 'warning', escalationStep: 1 }); expect(decide({ alert: warn, now: at(30) })).toEqual({ type: 'notify', routeId: 'peter-email', stepIndex: 2, nextStepDueAt: null, }); }); test('a disabled route is passed over the same way', () => { const routes = new Map(ROUTES); routes.set('wife-signal', { id: 'wife-signal', severityFloor: 'warning', enabled: false }); expect(decide({ alert: alert({ escalationStep: 1 }), routes, now: at(30) })).toMatchObject({ routeId: 'peter-email', }); }); // Distinct from policy_exhausted: the steps existed, nobody was eligible. test('reports no_eligible_route when every remaining route is filtered out', () => { const routes = new Map([ ['peter-signal', { id: 'peter-signal', severityFloor: 'critical', enabled: true }], ['wife-signal', { id: 'wife-signal', severityFloor: 'critical', enabled: true }], ['peter-email', { id: 'peter-email', severityFloor: 'critical', enabled: true }], ]); expect(decide({ alert: alert({ severity: 'warning' }), routes, now: at(60) })).toEqual({ type: 'skip', reason: 'no_eligible_route', }); }); // A route deleted out from under a policy must degrade, not crash — and it // reports no_eligible_route, since the step existed but had nobody to reach. test('an unknown route id is passed over rather than throwing', () => { const steps: EscalationStep[] = [{ stepIndex: 0, routeId: 'deleted', delayMinutes: 0 }]; expect(decide({ steps, now: at(5) })).toEqual({ type: 'skip', reason: 'no_eligible_route' }); }); }); describe('the escalation clock restarts after suppression', () => { // Suppressed at fire, un-suppressed 40 minutes later. If the clock still ran // from firstFiredAt, all three steps would fire at once — exactly the storm // suppression exists to prevent. test('a long suppression does not fire every step at once', () => { const unsuppressed = alert({ escalationStartedAt: at(40), graceUntil: at(40) }); expect(decide({ alert: unsuppressed, now: at(41) })).toEqual({ type: 'notify', routeId: 'peter-signal', stepIndex: 0, nextStepDueAt: at(50), }); }); test('the secondary still waits its full delay from the restart', () => { const unsuppressed = alert({ escalationStep: 1, escalationStartedAt: at(40), graceUntil: at(40), }); expect(decide({ alert: unsuppressed, now: at(45) })).toEqual({ type: 'skip', reason: 'not_yet_due', }); expect(decide({ alert: unsuppressed, now: at(50) })).toMatchObject({ routeId: 'wife-signal' }); }); });