import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { resolve } from 'node:path'; const execFileAsync = promisify(execFile); const HOOK_PATH = resolve(import.meta.dirname, '../../hooks/scripts/post-edit-verify.mjs'); /** * Run the hook with given stdin input (string). * Short timeout — these tests should only exercise fast paths. */ async function runHook(inputStr, timeout = 10000) { try { const { stdout } = await execFileAsync('node', [HOOK_PATH], { input: inputStr, timeout, }); return JSON.parse(stdout || '{}'); } catch (err) { if (err.stdout) { try { return JSON.parse(err.stdout); } catch { /* ignore */ } } return {}; } } // ======================================== // post-edit-verify hook — fast-path tests // Tests exercise the early-exit paths (non-config, missing file, invalid input). // Scanner execution paths are covered by scanner tests. // ======================================== describe('post-edit-verify hook', () => { it('returns {} for non-config files', async () => { const result = await runHook(JSON.stringify({ file_path: '/tmp/some-random-file.js' })); assert.deepEqual(result, {}); }); it('returns {} for nonexistent config files', async () => { const result = await runHook(JSON.stringify({ file_path: '/nonexistent/CLAUDE.md' })); assert.deepEqual(result, {}); }); it('returns {} for empty input object', async () => { const result = await runHook(JSON.stringify({})); assert.deepEqual(result, {}); }); it('returns {} for null file_path', async () => { const result = await runHook(JSON.stringify({ file_path: null })); assert.deepEqual(result, {}); }); it('handles invalid JSON gracefully', async () => { const result = await runHook('not json at all'); assert.deepEqual(result, {}); }); it('handles empty stdin gracefully', async () => { const result = await runHook(''); assert.deepEqual(result, {}); }); it('exits quickly for non-config files', async () => { const start = Date.now(); await runHook(JSON.stringify({ file_path: '/tmp/nothing.txt' })); const elapsed = Date.now() - start; // Non-config files exit before any scanner import — should be fast assert.ok(elapsed < 5000, `Hook took ${elapsed}ms for non-config file`); }); it('has correct shebang and is valid Node.js', async () => { const { readFile } = await import('node:fs/promises'); const content = await readFile(HOOK_PATH, 'utf-8'); assert.ok(content.startsWith('#!/usr/bin/env node')); assert.ok(content.includes('readFileSync')); assert.ok(content.includes('detectScanner')); }); it('uses cross-platform rules dir pattern (handles both / and \\)', async () => { const { readFile } = await import('node:fs/promises'); const content = await readFile(HOOK_PATH, 'utf-8'); // The regex should handle both Unix / and Windows \\ separators assert.ok(content.includes('[/\\\\]rules[/\\\\]'), 'RULES_DIR_PATTERN should match both / and \\\\'); }); });