import { describe, expect, test } from 'bun:test'; import type { DriftCategory, DriftFinding, SystemAuditReport } from '../../services/audit/types'; import { ALL_CATEGORIES, type AuditTuiState, CATEGORY_LABELS, groupFindings, initState, reducer, selectedFinding, } from './audit-state'; function finding(over: Partial): DriftFinding { return { category: 'module_configs', severity: 'drift', code: 'cfg_unset', subject: 'caddy', message: 'config "acme_ca" is unset', ...over, }; } function report(findings: DriftFinding[]): SystemAuditReport { return { version: 1, verdict: findings.some((f) => f.severity === 'blocked') ? 'BLOCKED' : findings.length > 0 ? 'DRIFT' : 'READY', generatedAt: '2026-04-25T12:00:00.000Z', findings, }; } describe('groupFindings', () => { test('buckets by category and sorts blocked first', () => { const groups = groupFindings([ finding({ category: 'module_configs', code: 'a', subject: 'caddy' }), finding({ category: 'capability_abi', severity: 'blocked', code: 'b', subject: 'public_web', }), finding({ category: 'module_configs', code: 'c', subject: 'iptables' }), ]); expect(groups.map((g) => g.category)).toEqual(['capability_abi', 'module_configs']); expect(groups[0].severity).toBe('blocked'); expect(groups[1].findings).toHaveLength(2); }); test('empty findings → empty groups', () => { expect(groupFindings([])).toEqual([]); }); }); describe('initState', () => { test('selects first category and first finding by default', () => { const s = initState( report([ finding({ category: 'backups', code: 'b1', subject: 'caddy' }), finding({ category: 'backups', code: 'b2', subject: 'iptables' }), ]), ); expect(s.selectedCategory).toBe('backups'); expect(s.selectedFindingId).toBe('caddy/b1'); expect(s.focusedPane).toBe('categories'); }); test('READY report with no findings has null selection', () => { const s = initState(report([])); expect(s.selectedCategory).toBeNull(); expect(s.selectedFindingId).toBeNull(); }); }); describe('reducer — drill-in (Enter)', () => { function setup(): AuditTuiState { return initState( report([ finding({ category: 'backups', code: 'b1', subject: 'caddy' }), finding({ category: 'module_configs', code: 'm1', subject: 'caddy' }), ]), ); } test('summary → categories', () => { const s = setup(); const next = reducer({ ...s, focusedPane: 'summary' }, { type: 'enter' }); expect(next.focusedPane).toBe('categories'); }); test('categories → findings (and selects first finding of category)', () => { const s = setup(); const next = reducer(s, { type: 'enter' }); expect(next.focusedPane).toBe('findings'); expect(next.selectedFindingId).toBe('caddy/b1'); }); test('findings → detail', () => { const s = setup(); const next = reducer({ ...s, focusedPane: 'findings' }, { type: 'enter' }); expect(next.focusedPane).toBe('detail'); }); test('detail → no-op (does not advance)', () => { const s = setup(); const next = reducer({ ...s, focusedPane: 'detail' }, { type: 'enter' }); expect(next.focusedPane).toBe('detail'); }); }); describe('reducer — step-back (Esc)', () => { function setup(): AuditTuiState { return initState(report([finding({ code: 'a' })])); } test('detail → findings', () => { const next = reducer({ ...setup(), focusedPane: 'detail' }, { type: 'escape' }); expect(next.focusedPane).toBe('findings'); }); test('findings → categories', () => { const next = reducer({ ...setup(), focusedPane: 'findings' }, { type: 'escape' }); expect(next.focusedPane).toBe('categories'); }); test('log → detail (down-then-over recovery)', () => { const next = reducer({ ...setup(), focusedPane: 'log' }, { type: 'escape' }); expect(next.focusedPane).toBe('detail'); }); test('categories → no-op (top-level handles quit)', () => { const s = { ...setup(), focusedPane: 'categories' as const }; const next = reducer(s, { type: 'escape' }); expect(next).toBe(s); }); }); describe('reducer — move within categories', () => { test('moving down updates selected category and resets finding', () => { const s = initState( report([ finding({ category: 'backups', code: 'b1', subject: 'caddy' }), finding({ category: 'capability_abi', severity: 'blocked', code: 'c1', subject: 'pw' }), ]), ); // initial: capability_abi (sorted blocked-first), c1 selected expect(s.selectedCategory).toBe('capability_abi'); expect(s.selectedFindingId).toBe('pw/c1'); const next = reducer({ ...s, focusedPane: 'categories' }, { type: 'move', direction: 1 }); expect(next.selectedCategory).toBe('backups'); expect(next.selectedFindingId).toBe('caddy/b1'); }); test('moving wraps around', () => { const s = initState( report([ finding({ category: 'backups', code: 'b1' }), finding({ category: 'module_configs', code: 'm1' }), ]), ); const down = reducer({ ...s, focusedPane: 'categories' }, { type: 'move', direction: 1 }); const wrap = reducer(down, { type: 'move', direction: 1 }); expect(wrap.selectedCategory).toBe(s.selectedCategory); }); }); describe('reducer — move within findings', () => { test('cycles within current category only', () => { const s = initState( report([ finding({ category: 'module_configs', code: 'a', subject: 'caddy' }), finding({ category: 'module_configs', code: 'b', subject: 'iptables' }), finding({ category: 'module_configs', code: 'c', subject: 'greenwave' }), ]), ); let st: AuditTuiState = { ...s, focusedPane: 'findings' }; expect(st.selectedFindingId).toBe('caddy/a'); st = reducer(st, { type: 'move', direction: 1 }); expect(st.selectedFindingId).toBe('iptables/b'); st = reducer(st, { type: 'move', direction: 1 }); expect(st.selectedFindingId).toBe('greenwave/c'); st = reducer(st, { type: 'move', direction: 1 }); expect(st.selectedFindingId).toBe('caddy/a'); }); }); describe('reducer — focus by number', () => { test('cannot focus findings when no category', () => { const s = initState(report([])); const next = reducer(s, { type: 'focus', pane: 'findings' }); expect(next.focusedPane).toBe(s.focusedPane); // unchanged }); test('focus pane succeeds when prerequisites met', () => { const s = initState(report([finding({ code: 'a' })])); const next = reducer(s, { type: 'focus', pane: 'detail' }); expect(next.focusedPane).toBe('detail'); }); }); describe('reducer — set-report preserves selection', () => { test('selection retained when finding still present', () => { const before = initState( report([ finding({ category: 'backups', code: 'b1', subject: 'caddy' }), finding({ category: 'backups', code: 'b2', subject: 'iptables' }), ]), ); // Select b2 const moved = reducer({ ...before, focusedPane: 'findings' }, { type: 'move', direction: 1 }); expect(moved.selectedFindingId).toBe('iptables/b2'); // Re-audit returns same findings const after = reducer(moved, { type: 'set-report', report: report([ finding({ category: 'backups', code: 'b1', subject: 'caddy' }), finding({ category: 'backups', code: 'b2', subject: 'iptables' }), ]), }); expect(after.selectedFindingId).toBe('iptables/b2'); }); test('selection falls back to first if missing after re-audit', () => { const before = initState(report([finding({ code: 'gone' })])); const after = reducer(before, { type: 'set-report', report: report([finding({ code: 'new', subject: 'other' })]), }); expect(after.selectedFindingId).toBe('other/new'); }); }); describe('selectedFinding', () => { test('returns the live finding object', () => { const s = initState(report([finding({ code: 'x', message: 'hello' })])); expect(selectedFinding(s)?.message).toBe('hello'); }); test('returns null with no selection', () => { expect(selectedFinding(initState(report([])))).toBeNull(); }); }); // ALL_CATEGORIES is a plain array, so the type system cannot require every // DriftCategory to appear in it — a new category compiles fine and then never // shows up in the TUI. CATEGORY_LABELS is a Record and IS exhaustive, so it is // the honest source of truth to compare against. describe('ALL_CATEGORIES covers every category', () => { test('every labelled category is listed, and vice versa', () => { expect([...ALL_CATEGORIES].sort()).toEqual( (Object.keys(CATEGORY_LABELS) as DriftCategory[]).sort(), ); }); });