import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import type { DbClient } from '../../db/client'; import { type Route, alerts, modules, monitors } from '../../db/schema'; import { setupTestDatabaseAt } from '../../test-utils/database'; import { resetTestDbPath } from '../../test-utils/db-path'; import { acknowledgeAlert, findLiveAlertByKey, resolveAlertManually, silenceAlert } from './ack'; import { moduleCheckAlertKey } from './keys'; import { createPerson, createRoute } from './people'; import { mintDelivery } from './tokens'; const NOW = new Date('2026-07-28T03:00:00Z'); const LATER = new Date('2026-07-28T04:00:00Z'); const KEY = moduleCheckAlertKey('caddy', 'disk-space'); describe('ack / silence / resolve', () => { let dir: string; let db: DbClient; let peterRoute: Route; let wifeRoute: Route; let peterId: string; let wifeId: string; beforeEach(async () => { dir = mkdtempSync(join(tmpdir(), 'ack-')); const dbPath = join(dir, 'celilo.db'); process.env.CELILO_DB_PATH = dbPath; db = await setupTestDatabaseAt(dbPath); db.insert(modules) .values({ id: 'signal', name: 'Signal', version: '0.1.0', manifestData: {}, sourcePath: '/tmp/signal', }) .run(); db.insert(monitors) .values({ id: 'mon-1', kind: 'module_hook', target: 'caddy', intervalMinutes: 15, severity: 'critical', }) .run(); db.insert(alerts) .values({ id: 'alert-1', key: KEY, activeKey: KEY, monitorId: 'mon-1', state: 'firing', severity: 'critical', graceUntil: NOW, message: '/var 94% used', }) .run(); const peter = createPerson(db, { name: 'peter', timezone: 'UTC' }); const wife = createPerson(db, { name: 'wife', timezone: 'UTC' }); peterId = peter.id; wifeId = wife.id; peterRoute = createRoute(db, { personId: peter.id, transportModuleId: 'signal', address: '+1555001', canAck: true, }); wifeRoute = createRoute(db, { personId: wife.id, transportModuleId: 'signal', address: '+1555002', canAck: true, }); }); afterEach(() => { db.$client.close(); resetTestDbPath(); try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } }); const page = (routeId: string) => mintDelivery(db, { kind: 'alert', targetId: 'alert-1', routeId, now: NOW, ttlMs: 86_400_000, }); describe('acknowledge', () => { // Ack stops escalation. It does NOT resolve — the problem is still // happening, someone is just dealing with it. test('sets acked state and records who and when, leaving it unresolved', () => { const result = acknowledgeAlert(db, 'alert-1', peterId, LATER); expect(result?.alert.state).toBe('acked'); expect(result?.alert.ackedBy).toBe(peterId); expect(result?.alert.ackedAt).toEqual(LATER); expect(result?.alert.resolvedAt).toBeNull(); expect(result?.alert.activeKey).toBe(KEY); }); // Without this the primary keeps believing they must act, which is the // duplicated effort the escalation chain exists to avoid. test("a secondary's ack is broadcast to everyone else who was paged", () => { page(peterRoute.id); page(wifeRoute.id); const result = acknowledgeAlert(db, 'alert-1', wifeId, LATER); expect(result?.broadcastTo).toEqual([peterRoute.id]); }); test('the acknowledger is not told about their own ack', () => { page(peterRoute.id); const result = acknowledgeAlert(db, 'alert-1', peterId, LATER); expect(result?.broadcastTo).toEqual([]); }); test('nobody paged means nobody to broadcast to', () => { const result = acknowledgeAlert(db, 'alert-1', peterId, LATER); expect(result?.broadcastTo).toEqual([]); }); test('duplicate deliveries to one route broadcast once', () => { page(peterRoute.id); page(peterRoute.id); const result = acknowledgeAlert(db, 'alert-1', wifeId, LATER); expect(result?.broadcastTo).toEqual([peterRoute.id]); }); // A reply arriving just after recovery is a normal race, not an error. test('acking an already-resolved alert is a no-op, not a failure', () => { resolveAlertManually(db, 'alert-1', LATER); const result = acknowledgeAlert(db, 'alert-1', peterId, LATER); expect(result).not.toBeNull(); expect(result?.alert.state).toBe('resolved'); }); test('an unknown alert returns null rather than throwing', () => { expect(acknowledgeAlert(db, 'nope', peterId, LATER)).toBeNull(); }); }); describe('silence', () => { // Silence is not ack: nobody owns the problem, they just do not want to // hear about it yet. test('records an expiry without acknowledging', () => { const alert = silenceAlert(db, 'alert-1', LATER); expect(alert?.silencedUntil).toEqual(LATER); expect(alert?.state).toBe('firing'); expect(alert?.ackedBy).toBeNull(); }); }); describe('manual resolve', () => { test('clears activeKey so the alert leaves the live set', () => { const alert = resolveAlertManually(db, 'alert-1', LATER); expect(alert?.state).toBe('resolved'); expect(alert?.activeKey).toBeNull(); expect(alert?.resolvedAt).toEqual(LATER); }); // Manual resolve cannot hide a real problem: the monitor is still the // authority, and a key that is still failing simply fires again. test('a still-failing key can re-fire afterwards', () => { resolveAlertManually(db, 'alert-1', LATER); expect(findLiveAlertByKey(db, KEY)).toBeUndefined(); db.insert(alerts) .values({ id: 'alert-2', key: KEY, activeKey: KEY, monitorId: 'mon-1', state: 'pending', severity: 'critical', graceUntil: LATER, message: '/var 96% used', }) .run(); expect(findLiveAlertByKey(db, KEY)?.id).toBe('alert-2'); }); }); describe('findLiveAlertByKey', () => { test('finds a live alert', () => { expect(findLiveAlertByKey(db, KEY)?.id).toBe('alert-1'); }); test('ignores resolved alerts', () => { resolveAlertManually(db, 'alert-1', LATER); expect(findLiveAlertByKey(db, KEY)).toBeUndefined(); }); test('an unknown key finds nothing', () => { expect(findLiveAlertByKey(db, 'module:nope')).toBeUndefined(); }); test('an acked alert is still live — it is not resolved', () => { acknowledgeAlert(db, 'alert-1', peterId, LATER); expect(findLiveAlertByKey(db, KEY)?.id).toBe('alert-1'); }); }); });