import { describe, expect, test } from 'bun:test'; import { render } from 'ink-testing-library'; import type { SystemAuditReport } from '../../services/audit/types'; import { AuditTui } from './audit-tui'; const FIXTURE: SystemAuditReport = { version: 1, verdict: 'BLOCKED', generatedAt: '2026-04-25T12:00:00.000Z', findings: [ { category: 'capability_abi', severity: 'blocked', code: 'pw_major_drift', subject: 'public_web', message: 'public_web@2.0.0 vs runtime 1.0.0', }, { category: 'module_configs', severity: 'drift', code: 'cfg_unset_acme_ca', subject: 'caddy', message: 'config "acme_ca" is unset', remediation: 'celilo module config set caddy acme_ca ', actionable: true, }, { category: 'module_configs', severity: 'drift', code: 'cfg_unset_nat_rules', subject: 'iptables', message: 'config "nat_rules" is unset', }, ], }; function strip(s: string): string { return s.replace(/\[[0-9;]*m/g, ''); } describe('AuditTui — initial frame', () => { test('renders all five pane headers and the keybar', () => { const { lastFrame } = render(); const out = strip(lastFrame() ?? ''); expect(out).toContain('1 Summary'); expect(out).toContain('2 Categories'); expect(out).toContain('3 Findings'); expect(out).toContain('4 Detail'); expect(out).toContain('5 Command log'); // Bottom bar shows the categories-pane bindings on initial focus expect(out).toContain('Focus: '); expect(out).toContain('? help · q quit'); }); test('shows the verdict and finding counts in the summary pane', () => { const { lastFrame } = render(); const out = strip(lastFrame() ?? ''); expect(out).toContain('BLOCKED'); expect(out).toContain('1 blocked, 2 drift'); }); test('shows the categories sorted blocked-first with counts', () => { const { lastFrame } = render(); const out = strip(lastFrame() ?? ''); // Match "× BLOCKED · capability_abi (1)" and "▲ DRIFT · module_configs (2)" // pattern in the categories pane. Use the bullet+count suffix to // disambiguate from the findings header text. const abiIdx = out.indexOf('capability_abi (1)'); const cfgIdx = out.indexOf('module_configs (2)'); expect(abiIdx).toBeGreaterThanOrEqual(0); expect(cfgIdx).toBeGreaterThanOrEqual(0); expect(abiIdx).toBeLessThan(cfgIdx); }); }); describe('AuditTui — navigation', () => { test('Enter from categories drills into findings', async () => { const { lastFrame, stdin } = render(); stdin.write('\r'); // Enter await new Promise((r) => setTimeout(r, 10)); const out = strip(lastFrame() ?? ''); // Findings-pane bottom bar shows "Back: " regardless of // whether the selected finding is actionable. expect(out).toContain('Back: '); }); test('? opens help overlay', async () => { const { lastFrame, stdin } = render(); stdin.write('?'); await new Promise((r) => setTimeout(r, 10)); const out = strip(lastFrame() ?? ''); expect(out).toContain('Keymap'); expect(out).toContain('Toggle this overlay'); }); test('moving down in categories pane changes selection (and findings update)', async () => { const { lastFrame, stdin } = render(); stdin.write('j'); // down await new Promise((r) => setTimeout(r, 10)); const out = strip(lastFrame() ?? ''); // After moving down, findings header should show module_configs expect(out).toContain('Findings · module_configs'); }); }); describe('AuditTui — remediation modal', () => { test('r on a finding with a remediation opens the modal pre-filled', async () => { const { lastFrame, stdin } = render(); // Move down to module_configs (which has a finding with remediation), // drill into Findings, then press 'r'. stdin.write('j'); await new Promise((r) => setTimeout(r, 10)); stdin.write('\r'); await new Promise((r) => setTimeout(r, 10)); stdin.write('r'); await new Promise((r) => setTimeout(r, 10)); const out = strip(lastFrame() ?? ''); expect(out).toContain('Remediate'); expect(out).toContain('celilo module config set caddy acme_ca'); }); test('keybar shows modal-specific bindings while remediate modal is open', async () => { const { lastFrame, stdin } = render(); stdin.write('j'); await new Promise((r) => setTimeout(r, 10)); stdin.write('\r'); await new Promise((r) => setTimeout(r, 10)); stdin.write('r'); await new Promise((r) => setTimeout(r, 10)); const out = strip(lastFrame() ?? ''); expect(out).toContain('Run: '); expect(out).toContain('Cancel: '); }); });