// pre-compact-scan.test.mjs — Tests for hooks/scripts/pre-compact-scan.mjs // Covers PreCompact event handling with three modes: block, warn, off. // Verifies transcript scanning for injection patterns and credentials. // Verifies size-cap behavior (500 KB tail) stays under 500 ms for large files. // // Credential fixture is generated programmatically in before() to avoid // pre-edit-secrets false-positive on the test file itself. import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { resolve, dirname } from 'node:path'; import { writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs'; import { runHook, runHookWithEnv } from './hook-helper.mjs'; const SCRIPT = resolve(import.meta.dirname, '../../hooks/scripts/pre-compact-scan.mjs'); const FIXTURE_DIR = resolve(import.meta.dirname, '../fixtures/transcripts'); const CLEAN = resolve(FIXTURE_DIR, 'clean.jsonl'); const WITH_INJECTION = resolve(FIXTURE_DIR, 'with-injection.jsonl'); const WITH_CREDENTIAL = resolve(FIXTURE_DIR, 'with-credential.jsonl'); const LARGE = resolve(FIXTURE_DIR, 'large.jsonl'); function payload(transcriptPath, extra = {}) { return { session_id: 'test-session', transcript_path: transcriptPath, cwd: process.cwd(), hook_event_name: 'PreCompact', trigger: 'auto', ...extra, }; } function parseOutput(stdout) { const trimmed = stdout.trim(); if (!trimmed) return null; try { return JSON.parse(trimmed); } catch { return null; } } describe('pre-compact-scan hook', () => { before(() => { if (!existsSync(FIXTURE_DIR)) mkdirSync(FIXTURE_DIR, { recursive: true }); // Credential fixture built at runtime so the repo never contains a literal // secret-like string that would trip pre-edit-secrets. const keyPrefix = 'AKI' + 'A'; const body = 'A1B2C3D4E5F6G7H8'; const credLine = JSON.stringify({ type: 'user', message: { role: 'user', content: `Config: AWS_KEY=${keyPrefix}${body}` }, }); writeFileSync(WITH_CREDENTIAL, credLine + '\n'); // Large transcript fixture — ~1.2 MB of benign filler. const filler = JSON.stringify({ type: 'user', message: { role: 'user', content: 'benign content '.repeat(200) }, }); const lines = []; for (let i = 0; i < 800; i++) lines.push(filler); writeFileSync(LARGE, lines.join('\n')); }); after(() => { try { rmSync(WITH_CREDENTIAL); } catch {} try { rmSync(LARGE); } catch {} }); it('clean transcript in warn mode exits 0 with no systemMessage', async () => { const r = await runHookWithEnv(SCRIPT, payload(CLEAN), { LLM_SECURITY_PRECOMPACT_MODE: 'warn' }); assert.equal(r.code, 0); assert.equal(parseOutput(r.stdout), null); }); it('injection pattern in warn mode exits 0 with systemMessage', async () => { const r = await runHookWithEnv(SCRIPT, payload(WITH_INJECTION), { LLM_SECURITY_PRECOMPACT_MODE: 'warn' }); assert.equal(r.code, 0); const out = parseOutput(r.stdout); assert.ok(out, 'expected systemMessage JSON on stdout'); assert.ok(typeof out.systemMessage === 'string' && out.systemMessage.length > 0); assert.match(out.systemMessage, /finding/); }); it('injection pattern in block mode exits 2', async () => { const r = await runHookWithEnv(SCRIPT, payload(WITH_INJECTION), { LLM_SECURITY_PRECOMPACT_MODE: 'block' }); assert.equal(r.code, 2); const out = parseOutput(r.stdout); assert.ok(out, 'expected block JSON on stdout'); assert.equal(out.decision, 'block'); }); it('injection pattern in off mode exits 0 with no output', async () => { const r = await runHookWithEnv(SCRIPT, payload(WITH_INJECTION), { LLM_SECURITY_PRECOMPACT_MODE: 'off' }); assert.equal(r.code, 0); assert.equal(parseOutput(r.stdout), null); }); it('size-cap: ~1MB transcript completes under 500 ms', async () => { const start = process.hrtime.bigint(); const r = await runHookWithEnv(SCRIPT, payload(LARGE), { LLM_SECURITY_PRECOMPACT_MODE: 'warn' }); const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6; assert.equal(r.code, 0, 'hook should not fail on large transcript'); assert.ok(elapsedMs < 500, `expected <500 ms, got ${elapsedMs.toFixed(1)} ms`); }); it('credential pattern in transcript is detected in warn mode', async () => { const r = await runHookWithEnv(SCRIPT, payload(WITH_CREDENTIAL), { LLM_SECURITY_PRECOMPACT_MODE: 'warn' }); assert.equal(r.code, 0); const out = parseOutput(r.stdout); assert.ok(out, 'expected systemMessage JSON on stdout'); assert.match(out.systemMessage, /AWS|finding/); }); });