// reference-config.test.mjs — Tests for the reference configuration generator // Tests against fixtures in tests/fixtures/posture-scan/ with: // - grade-a-project: already Grade A → no recommendations // - grade-f-project: dangerous flags, no hooks → full recommendations import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { resolve, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from 'node:fs'; import { resetCounter } from '../../scanners/lib/output.mjs'; import { generate } from '../../scanners/reference-config-generator.mjs'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const GRADE_A_FIXTURE = resolve(__dirname, '../fixtures/posture-scan/grade-a-project'); const GRADE_F_FIXTURE = resolve(__dirname, '../fixtures/posture-scan/grade-f-project'); // --------------------------------------------------------------------------- // Grade A project — already well-configured, no changes needed // --------------------------------------------------------------------------- describe('reference-config-generator: grade-a-project', () => { let result; beforeEach(async () => { resetCounter(); result = await generate(GRADE_A_FIXTURE); }); it('returns status ok', () => { assert.equal(result.status, 'ok'); }); it('detects project type as standalone', () => { assert.equal(result.projectType, 'standalone'); }); it('includes posture grade', () => { assert.equal(result.posture.grade, 'A'); }); it('has zero or minimal recommendations', () => { // Grade A should need very few changes (maybe none) const actionable = result.recommendations.filter(r => r.action !== 'none'); assert.ok(actionable.length <= 2, `Expected <= 2 actionable, got ${actionable.length}`); }); it('does not recommend settings.json changes', () => { const settingsRec = result.recommendations.find(r => r.file === '.claude/settings.json' && r.action !== 'none'); assert.equal(settingsRec, undefined, 'Should not recommend settings changes for Grade A'); }); it('applied is false by default (dry-run)', () => { assert.equal(result.applied, false); }); }); // --------------------------------------------------------------------------- // Grade F project — needs everything // --------------------------------------------------------------------------- describe('reference-config-generator: grade-f-project', () => { let result; beforeEach(async () => { resetCounter(); result = await generate(GRADE_F_FIXTURE); }); it('returns status ok', () => { assert.equal(result.status, 'ok'); }); it('detects project type', () => { assert.ok( ['plugin', 'standalone', 'monorepo'].includes(result.projectType), `Expected valid project type, got ${result.projectType}`, ); }); it('includes posture grade F', () => { assert.equal(result.posture.grade, 'F'); }); it('recommends settings.json with deny-first', () => { const rec = result.recommendations.find(r => r.file === '.claude/settings.json'); assert.ok(rec, 'Expected settings.json recommendation'); assert.ok(rec.action === 'create' || rec.action === 'merge', `Expected create or merge, got ${rec.action}`); assert.ok(rec.content.includes('deny'), 'Settings should include deny-first'); }); it('recommends CLAUDE.md security section', () => { const rec = result.recommendations.find(r => r.file === 'CLAUDE.md'); assert.ok(rec, 'Expected CLAUDE.md recommendation'); assert.ok(rec.content.includes('Security Boundaries'), 'Should include security boundaries'); }); it('recommends .gitignore additions', () => { const rec = result.recommendations.find(r => r.file === '.gitignore'); assert.ok(rec, 'Expected .gitignore recommendation'); assert.ok(rec.content.includes('.env'), 'Should include .env'); }); it('has multiple recommendations', () => { const actionable = result.recommendations.filter(r => r.action !== 'none'); assert.ok(actionable.length >= 3, `Expected >= 3 actionable, got ${actionable.length}`); }); it('each recommendation has required fields', () => { for (const rec of result.recommendations) { assert.ok(rec.category, `Missing category in recommendation`); assert.ok(rec.file, `Missing file in recommendation`); assert.ok(rec.action, `Missing action in recommendation`); assert.ok(typeof rec.content === 'string', `Missing content in recommendation`); } }); }); // --------------------------------------------------------------------------- // Apply mode — writes files to a temp directory // --------------------------------------------------------------------------- describe('reference-config-generator: --apply mode', () => { const tmpDir = resolve(__dirname, '../fixtures/posture-scan/tmp-apply-test'); beforeEach(() => { // Create a bare project to apply to rmSync(tmpDir, { recursive: true, force: true }); mkdirSync(tmpDir, { recursive: true }); writeFileSync(join(tmpDir, 'CLAUDE.md'), '# Test Project\n\nA bare project.\n'); }); it('creates settings.json when applying', async () => { resetCounter(); const result = await generate(tmpDir, { apply: true }); assert.equal(result.applied, true); const settingsPath = join(tmpDir, '.claude', 'settings.json'); assert.ok(existsSync(settingsPath), 'settings.json should exist after apply'); const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); assert.equal(settings.permissions.defaultPermissionLevel, 'deny'); }); it('appends security section to existing CLAUDE.md', async () => { resetCounter(); await generate(tmpDir, { apply: true }); const content = readFileSync(join(tmpDir, 'CLAUDE.md'), 'utf-8'); assert.ok(content.includes('Security Boundaries'), 'CLAUDE.md should have security section'); assert.ok(content.includes('# Test Project'), 'Original content should be preserved'); }); it('creates .gitignore with security patterns', async () => { resetCounter(); await generate(tmpDir, { apply: true }); const content = readFileSync(join(tmpDir, '.gitignore'), 'utf-8'); assert.ok(content.includes('.env'), '.gitignore should include .env'); assert.ok(content.includes('*.key'), '.gitignore should include *.key'); }); it('does not overwrite existing settings.json', async () => { // Create existing settings mkdirSync(join(tmpDir, '.claude'), { recursive: true }); writeFileSync(join(tmpDir, '.claude', 'settings.json'), JSON.stringify({ permissions: { defaultPermissionLevel: 'deny', allow: ['Read(*)'] }, customKey: 'preserved', }, null, 2)); resetCounter(); const result = await generate(tmpDir, { apply: true }); const settings = JSON.parse(readFileSync(join(tmpDir, '.claude', 'settings.json'), 'utf-8')); assert.equal(settings.customKey, 'preserved', 'Custom keys should be preserved'); }); it('reports backupPath when applying', async () => { resetCounter(); const result = await generate(tmpDir, { apply: true }); // backupPath is set when there were existing files to back up // For a bare project, it may or may not create backup assert.ok(typeof result.backupPath === 'string' || result.backupPath === null); }); // Cleanup it('cleanup', () => { rmSync(tmpDir, { recursive: true, force: true }); }); }); // --------------------------------------------------------------------------- // Project type detection // --------------------------------------------------------------------------- describe('reference-config-generator: project type detection', () => { const tmpDir = resolve(__dirname, '../fixtures/posture-scan/tmp-type-test'); it('detects plugin project', async () => { rmSync(tmpDir, { recursive: true, force: true }); mkdirSync(join(tmpDir, '.claude-plugin'), { recursive: true }); writeFileSync(join(tmpDir, '.claude-plugin', 'plugin.json'), '{"name":"test"}'); resetCounter(); const result = await generate(tmpDir); assert.equal(result.projectType, 'plugin'); rmSync(tmpDir, { recursive: true, force: true }); }); it('detects monorepo', async () => { rmSync(tmpDir, { recursive: true, force: true }); mkdirSync(tmpDir, { recursive: true }); writeFileSync(join(tmpDir, 'package.json'), '{"workspaces":["packages/*"]}'); resetCounter(); const result = await generate(tmpDir); assert.equal(result.projectType, 'monorepo'); rmSync(tmpDir, { recursive: true, force: true }); }); it('defaults to standalone', async () => { rmSync(tmpDir, { recursive: true, force: true }); mkdirSync(tmpDir, { recursive: true }); resetCounter(); const result = await generate(tmpDir); assert.equal(result.projectType, 'standalone'); rmSync(tmpDir, { recursive: true, force: true }); }); });