import { describe, it, expect } from 'vitest' import { buildAuditEvent, audit } from '../src/audit/log.js' import { assertZeroPayload, ZeroPayloadViolation } from '../src/audit/redact.js' import type { AuditEvent, AuditSink } from '../src/types.js' import { principal, fakeAuditSink } from './_helpers.js' const NOW = 1_700_000_000 describe('audit log (INV10)', () => { it('builds a well-formed attach allow event and appends it', async () => { const sink = fakeAuditSink() const e = buildAuditEvent({ action: 'attach', principal: principal('acct-A'), hostId: 'host-1', sessionId: null, jti: 'jti-1', outcome: 'allow', reason: 'ok', remoteAddrHash: 'hash', now: NOW, }) await audit(sink, e) expect(sink.events).toHaveLength(1) expect(sink.events[0]!.action).toBe('attach') expect(sink.events[0]!.accountId).toBe('acct-A') }) it('records a deny event with a reason', () => { const e = buildAuditEvent({ action: 'cross-tenant-attempt', principal: null, hostId: 'host-B', sessionId: null, jti: null, outcome: 'deny', reason: 'cross_tenant', remoteAddrHash: 'hash', now: NOW, }) expect(e.outcome).toBe('deny') expect(e.reason).toBe('cross_tenant') expect(e.principalId).toBe('') }) it('throws (zero-payload guard) when a field contains ESC bytes', () => { const e: AuditEvent = { ts: new Date(NOW * 1000).toISOString(), action: 'attach', principalId: 'p', accountId: 'a', hostId: null, sessionId: null, jti: null, outcome: 'allow', reason: 'output:' + String.fromCharCode(0x1b) + '[31mred', remoteAddrHash: 'h', } expect(() => assertZeroPayload(e)).toThrow(ZeroPayloadViolation) }) it('throws when a field exceeds the metadata length cap', () => { const e: AuditEvent = { ts: new Date(NOW * 1000).toISOString(), action: 'attach', principalId: 'p', accountId: 'a', hostId: null, sessionId: null, jti: null, outcome: 'allow', reason: 'x'.repeat(300), remoteAddrHash: 'h', } expect(() => assertZeroPayload(e)).toThrow(ZeroPayloadViolation) }) it('audit() refuses to append a payload-bearing event', async () => { const bad: AuditEvent = { ts: new Date(NOW * 1000).toISOString(), action: 'attach', principalId: 'p', accountId: 'a', hostId: ']0;title', sessionId: null, jti: null, outcome: 'allow', reason: 'ok', remoteAddrHash: 'h', } const sink: AuditSink = { append: async () => undefined } await expect(audit(sink, bad)).rejects.toThrow(ZeroPayloadViolation) }) })