// tests/hooks/post-compact-flush.test.mjs // Step 13 (plan-v2) — PostCompact rehydrate hook test. // // Hook is read-only: discovers /.claude/projects/*/.session-state.local.json, // validates it, emits additionalContext for the post-compact assistant turn. // Must always exit 0 — never blocks compaction. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFile } from 'node:child_process'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const HOOK = join(ROOT, 'hooks', 'scripts', 'post-compact-flush.mjs'); function runHookIn(cwd, input = {}) { return new Promise((resolve) => { const child = execFile( 'node', [HOOK], { timeout: 5000, cwd, env: { ...process.env } }, (err, stdout, stderr) => { resolve({ code: child.exitCode ?? 0, stdout: stdout || '', stderr: stderr || '', }); }, ); child.stdin.end(typeof input === 'string' ? input : JSON.stringify(input)); }); } function makeFixture() { const dir = mkdtempSync(join(tmpdir(), 'post-compact-flush-')); return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) }; } test('post-compact-flush: exits 0 with empty output when no .claude/projects/ exists', async () => { const { dir, cleanup } = makeFixture(); try { const { code, stdout } = await runHookIn(dir); assert.strictEqual(code, 0, 'hook must always exit 0 — never blocks compaction'); assert.strictEqual(stdout, '{}', 'no state file → emit empty payload (silent no-op)'); } finally { cleanup(); } }); test('post-compact-flush: exits 0 with empty output when state file is malformed', async () => { const { dir, cleanup } = makeFixture(); try { mkdirSync(join(dir, '.claude/projects/2026-05-04-test'), { recursive: true }); writeFileSync( join(dir, '.claude/projects/2026-05-04-test/.session-state.local.json'), '{not valid json', ); const { code, stdout } = await runHookIn(dir); assert.strictEqual(code, 0, 'malformed state file → silent fail, exit 0'); assert.strictEqual(stdout, '{}', 'no additionalContext on malformed input'); } finally { cleanup(); } }); test('post-compact-flush: emits additionalContext with project + next_session_label + status from valid state file', async () => { const { dir, cleanup } = makeFixture(); try { mkdirSync(join(dir, '.claude/projects/2026-05-04-test'), { recursive: true }); const state = { schema_version: 1, project: '.claude/projects/2026-05-04-test', next_session_brief_path: '.claude/projects/2026-05-04-test/brief.md', next_session_label: 'Session 9: Wave 2 manual delivery', status: 'in_progress', updated_at: '2026-05-04T07:00:00.000Z', }; writeFileSync( join(dir, '.claude/projects/2026-05-04-test/.session-state.local.json'), JSON.stringify(state, null, 2), ); const { code, stdout } = await runHookIn(dir); assert.strictEqual(code, 0, 'valid state → exit 0'); const parsed = JSON.parse(stdout); assert.ok(parsed.additionalContext, 'must emit additionalContext for the next turn'); assert.match(parsed.additionalContext, /\.claude\/projects\/2026-05-04-test/, 'context includes project path'); assert.match(parsed.additionalContext, /Session 9: Wave 2 manual delivery/, 'context includes next_session_label'); assert.match(parsed.additionalContext, /status: in_progress/, 'context includes status'); } finally { cleanup(); } }); test('post-compact-flush: picks the most-recently-modified state file when multiple projects exist', async () => { const { dir, cleanup } = makeFixture(); try { mkdirSync(join(dir, '.claude/projects/older'), { recursive: true }); mkdirSync(join(dir, '.claude/projects/newer'), { recursive: true }); const baseState = (label) => ({ schema_version: 1, project: `.claude/projects/${label}`, next_session_brief_path: `.claude/projects/${label}/brief.md`, next_session_label: `Label-${label}`, status: 'in_progress', updated_at: '2026-05-04T07:00:00.000Z', }); const olderPath = join(dir, '.claude/projects/older/.session-state.local.json'); const newerPath = join(dir, '.claude/projects/newer/.session-state.local.json'); writeFileSync(olderPath, JSON.stringify(baseState('older'))); // Wait one tick to ensure mtime ordering is observable on all filesystems await new Promise((r) => setTimeout(r, 50)); writeFileSync(newerPath, JSON.stringify(baseState('newer'))); const { code, stdout } = await runHookIn(dir); assert.strictEqual(code, 0); const parsed = JSON.parse(stdout); assert.match(parsed.additionalContext, /Label-newer/, 'auto-discovery should pick the newest state file'); assert.doesNotMatch(parsed.additionalContext, /Label-older/, 'older state file must not be selected'); } finally { cleanup(); } });