import { describe, it, expect } from 'vitest'; import { ConsoleAuditLogger, MemoryAuditLogger, NoopAuditLogger, hashArgs, type AuditRecord, } from './audit.js'; const sample: AuditRecord = { sub: 'u@seahavenind.com', tool: 'lookup_payment', argsHash: 'abc', decision: 'allow', result: 'ok', ts: '2026-06-24T00:00:00.000Z', }; describe('hashArgs', () => { it('produces a stable 64-char hex digest', () => { const h = hashArgs({ vendor: 'Acme', amount: 5 }); expect(h).toMatch(/^[0-9a-f]{64}$/); }); it('is order-insensitive for object keys (canonicalized)', () => { expect(hashArgs({ a: 1, b: 2 })).toBe(hashArgs({ b: 2, a: 1 })); }); it('differs when values differ and never embeds the raw value', () => { const h1 = hashArgs({ secret: 'TOPSECRET' }); const h2 = hashArgs({ secret: 'other' }); expect(h1).not.toBe(h2); expect(h1).not.toContain('TOPSECRET'); }); }); describe('MemoryAuditLogger', () => { it('captures records in order', () => { const log = new MemoryAuditLogger(); log.log(sample); log.log({ ...sample, tool: 'second' }); expect(log.records.map((r) => r.tool)).toEqual(['lookup_payment', 'second']); }); }); describe('NoopAuditLogger', () => { it('accepts records without throwing', () => { expect(() => new NoopAuditLogger().log(sample)).not.toThrow(); }); }); describe('ConsoleAuditLogger', () => { it('writes one structured JSON line tagged as audit', () => { const lines: string[] = []; const orig = console.log; // eslint-disable-next-line no-console console.log = (msg?: unknown) => void lines.push(String(msg)); try { new ConsoleAuditLogger().log(sample); } finally { // eslint-disable-next-line no-console console.log = orig; } expect(lines).toHaveLength(1); const parsed = JSON.parse(lines[0]!) as Record; expect(parsed['kind']).toBe('audit'); expect(parsed['tool']).toBe('lookup_payment'); }); });