import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { verifyConfig as main, type VerifyConfigOptions } from '../src/bin/verify-config.ts'; describe('verify-config', () => { let tempDir: string; let userConfigPath: string; let projectConfigPath: string; let capturedStdout: string[]; let capturedStderr: string[]; let originalConsoleLog: typeof console.log; let originalConsoleError: typeof console.error; beforeEach(() => { // Create unique temp directory tempDir = join( tmpdir(), `verify-config-test-${Date.now()}-${Math.random().toString(36).slice(2)}`, ); mkdirSync(tempDir, { recursive: true }); // Set up paths userConfigPath = join(tempDir, '.cc-safety-net', 'config.json'); projectConfigPath = join(tempDir, '.safety-net.json'); // Capture console output capturedStdout = []; capturedStderr = []; originalConsoleLog = console.log; originalConsoleError = console.error; console.log = (...args: unknown[]) => { capturedStdout.push(args.map(String).join(' ')); }; console.error = (...args: unknown[]) => { capturedStderr.push(args.map(String).join(' ')); }; }); afterEach(() => { // Restore console console.log = originalConsoleLog; console.error = originalConsoleError; // Clean up temp directory if (existsSync(tempDir)) { rmSync(tempDir, { recursive: true, force: true }); } }); function writeUserConfig(content: string): void { const dir = join(tempDir, '.cc-safety-net'); mkdirSync(dir, { recursive: true }); writeFileSync(userConfigPath, content, 'utf-8'); } function writeProjectConfig(content: string): void { writeFileSync(projectConfigPath, content, 'utf-8'); } function runMain(): number { const options: VerifyConfigOptions = { userConfigPath, projectConfigPath, }; return main(options); } function getStdout(): string { return capturedStdout.join('\n'); } function getStderr(): string { return capturedStderr.join('\n'); } describe('no configs', () => { test('returns zero when no configs exist', () => { const result = runMain(); expect(result).toBe(0); }); test('prints header', () => { runMain(); const output = getStdout(); expect(output).toContain('Safety Net Config'); expect(output).toContain('═'); }); test('prints no configs message', () => { runMain(); const output = getStdout(); expect(output).toContain('No config files found'); expect(output).toContain('Using built-in rules only'); }); }); describe('valid configs', () => { test('user config only returns zero', () => { writeUserConfig('{"version": 1}'); const result = runMain(); expect(result).toBe(0); }); test('user config prints checkmark', () => { writeUserConfig('{"version": 1}'); runMain(); const output = getStdout(); expect(output).toContain('✓ User config:'); }); test('user config shows rules none', () => { writeUserConfig('{"version": 1}'); runMain(); const output = getStdout(); expect(output).toContain('Rules: (none)'); }); test('user config with rules shows numbered list', () => { writeUserConfig( JSON.stringify({ version: 1, rules: [ { name: 'block-foo', command: 'foo', block_args: ['-x'], reason: 'Blocked', }, { name: 'block-bar', command: 'bar', block_args: ['-y'], reason: 'Blocked', }, ], }), ); runMain(); const output = getStdout(); expect(output).toContain('Rules:'); expect(output).toContain('1. block-foo'); expect(output).toContain('2. block-bar'); }); test('project config only returns zero', () => { writeProjectConfig('{"version": 1}'); const result = runMain(); expect(result).toBe(0); }); test('project config prints checkmark', () => { writeProjectConfig('{"version": 1}'); runMain(); const output = getStdout(); expect(output).toContain('✓ Project config:'); }); test('both configs returns zero', () => { writeUserConfig('{"version": 1}'); writeProjectConfig('{"version": 1}'); const result = runMain(); expect(result).toBe(0); }); test('both configs prints both checkmarks', () => { writeUserConfig('{"version": 1}'); writeProjectConfig('{"version": 1}'); runMain(); const output = getStdout(); expect(output).toContain('✓ User config:'); expect(output).toContain('✓ Project config:'); }); test('valid config prints success message', () => { writeProjectConfig('{"version": 1}'); runMain(); const output = getStdout(); expect(output).toContain('All configs valid.'); }); }); describe('invalid configs', () => { test('invalid user config returns one', () => { writeUserConfig('{"version": 2}'); const result = runMain(); expect(result).toBe(1); }); test('invalid user config prints x mark', () => { writeUserConfig('{"version": 2}'); runMain(); const output = getStderr(); expect(output).toContain('✗ User config:'); }); test('invalid config shows numbered errors', () => { writeUserConfig('{"version": 2}'); runMain(); const output = getStderr(); expect(output).toContain('Errors:'); expect(output).toContain('1.'); expect(output).toContain('version'); }); test('invalid project config returns one', () => { writeProjectConfig('{"rules": []}'); const result = runMain(); expect(result).toBe(1); }); test('invalid project config prints x mark', () => { writeProjectConfig('{"rules": []}'); runMain(); const output = getStderr(); expect(output).toContain('✗ Project config:'); }); test('both invalid returns one', () => { writeUserConfig('{"version": 2}'); writeProjectConfig('{"rules": []}'); const result = runMain(); expect(result).toBe(1); }); test('both invalid prints both errors', () => { writeUserConfig('{"version": 2}'); writeProjectConfig('{"rules": []}'); runMain(); const output = getStderr(); expect(output).toContain('✗ User config:'); expect(output).toContain('✗ Project config:'); }); test('invalid json prints error', () => { writeProjectConfig('{ not valid json }'); runMain(); const output = getStderr(); expect(output).toContain('✗ Project config:'); }); test('validation failed message', () => { writeProjectConfig('{"version": 2}'); runMain(); const output = getStderr(); expect(output).toContain('Config validation failed.'); }); }); describe('mixed validity', () => { test('valid user invalid project returns one', () => { writeUserConfig('{"version": 1}'); writeProjectConfig('{"version": 2}'); const result = runMain(); expect(result).toBe(1); }); test('valid user invalid project shows both', () => { writeUserConfig('{"version": 1}'); writeProjectConfig('{"version": 2}'); runMain(); const stdout = getStdout(); const stderr = getStderr(); expect(stdout).toContain('✓ User config:'); expect(stderr).toContain('✗ Project config:'); }); test('invalid user valid project returns one', () => { writeUserConfig('{"version": 2}'); writeProjectConfig('{"version": 1}'); const result = runMain(); expect(result).toBe(1); }); test('invalid user valid project shows both', () => { writeUserConfig('{"version": 2}'); writeProjectConfig('{"version": 1}'); runMain(); const stdout = getStdout(); const stderr = getStderr(); expect(stderr).toContain('✗ User config:'); expect(stdout).toContain('✓ Project config:'); }); }); describe('schema auto-add', () => { function readProjectConfig(): Record { return JSON.parse(readFileSync(projectConfigPath, 'utf-8')); } function readUserConfig(): Record { return JSON.parse(readFileSync(userConfigPath, 'utf-8')); } test('adds $schema to valid project config missing it', () => { writeProjectConfig('{"version": 1}'); runMain(); const config = readProjectConfig(); expect(config.$schema).toBe( 'https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json', ); }); test('adds $schema to valid user config missing it', () => { writeUserConfig('{"version": 1}'); runMain(); const config = readUserConfig(); expect(config.$schema).toBe( 'https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json', ); }); test('does not modify config that already has $schema', () => { const originalConfig = { $schema: 'https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json', version: 1, }; writeProjectConfig(JSON.stringify(originalConfig, null, 2)); runMain(); const config = readProjectConfig(); expect(config).toEqual(originalConfig); }); test('preserves existing rules when adding $schema', () => { const originalConfig = { version: 1, rules: [ { name: 'block-foo', command: 'foo', block_args: ['-x'], reason: 'Blocked', }, ], }; writeProjectConfig(JSON.stringify(originalConfig)); runMain(); const config = readProjectConfig(); expect(config.$schema).toBe( 'https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json', ); expect(config.version).toBe(1); expect(config.rules).toEqual(originalConfig.rules); }); test('does not add $schema to invalid config', () => { writeProjectConfig('{"version": 2}'); runMain(); const config = readProjectConfig(); expect(config.$schema).toBeUndefined(); }); test('prints message when $schema is added', () => { writeProjectConfig('{"version": 1}'); runMain(); const output = getStdout(); expect(output).toContain('Added $schema'); }); }); });