import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { generatePostureReport, generateDriftReport, generatePluginHealthReport, generateFullReport, } from '../../scanners/lib/report-generator.mjs'; // --- Helpers --- function makePostureResult(overrides = {}) { return { utilization: { score: 65, overhang: 35 }, maturity: { level: 2, name: 'Structured', description: 'Rules, skills, hooks' }, segment: { segment: 'Strong', description: 'Well-configured' }, areas: [ { name: 'CLAUDE.md', grade: 'A', score: 95, findingCount: 0 }, { name: 'Settings', grade: 'B', score: 80, findingCount: 2 }, { name: 'Hooks', grade: 'C', score: 60, findingCount: 4 }, ], overallGrade: 'B', topActions: ['Add MCP server', 'Configure hooks diversity', 'Add custom skills'], scannerEnvelope: { meta: { target: '/test/project', timestamp: '2026-04-03T12:00:00.000Z', version: '2.0.0', tool: 'config-audit' }, scanners: [ { scanner: 'CML', findings: [], counts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 } }, { scanner: 'SET', findings: [ { severity: 'medium', title: 'Unknown key', file: 'settings.json' }, { severity: 'low', title: 'Missing schema', file: 'settings.json' }, ], counts: { critical: 0, high: 0, medium: 1, low: 1, info: 0 } }, ], }, ...overrides, }; } function makeDiffResult(overrides = {}) { return { summary: { totalBefore: 10, totalAfter: 8, newCount: 1, resolvedCount: 3, trend: 'improving' }, scoreChange: { before: { score: 60, grade: 'C' }, after: { score: 75, grade: 'B' }, delta: 15, }, newFindings: [ { severity: 'medium', title: 'New finding', file: 'test.json' }, ], resolvedFindings: [ { severity: 'high', title: 'Fixed issue' }, { severity: 'medium', title: 'Another fixed' }, { severity: 'low', title: 'Minor fix' }, ], areaChanges: [ { name: 'Settings', before: { score: 60, grade: 'C' }, after: { score: 80, grade: 'B' }, delta: 20 }, { name: 'Hooks', before: { score: 70, grade: 'B' }, after: { score: 70, grade: 'B' }, delta: 0 }, ], ...overrides, }; } function makePluginResults() { return [ { name: 'plugin-a', findings: [], commandCount: 5, agentCount: 2 }, { name: 'plugin-b', findings: [ { severity: 'medium', title: 'Missing frontmatter' }, { severity: 'low', title: 'No README' }, ], commandCount: 3, agentCount: 1 }, ]; } function makeScanResult(crossPluginFindings = []) { return { scanner: 'PLH', status: 'ok', findings: [...crossPluginFindings], counts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 }, }; } // ======================================== // generatePostureReport // ======================================== describe('generatePostureReport', () => { it('returns markdown with health header and grade', () => { const report = generatePostureReport(makePostureResult()); assert.ok(report.includes('## Health Assessment')); assert.ok(report.includes('Health Grade')); assert.ok(report.includes('**B**')); }); it('includes area breakdown table (quality areas only)', () => { const report = generatePostureReport(makePostureResult()); assert.ok(report.includes('| CLAUDE.md |')); assert.ok(report.includes('| Settings |')); assert.ok(report.includes('| Hooks |')); }); it('excludes Feature Coverage from area breakdown', () => { const result = makePostureResult({ areas: [ { name: 'CLAUDE.md', grade: 'A', score: 95, findingCount: 0 }, { name: 'Feature Coverage', grade: 'F', score: 20, findingCount: 15 }, ], }); const report = generatePostureReport(result); assert.ok(!report.includes('| Feature Coverage |')); assert.ok(report.includes('| CLAUDE.md |')); }); it('shows opportunity count when present', () => { const report = generatePostureReport(makePostureResult({ opportunityCount: 5 })); assert.ok(report.includes('5 features available')); }); it('does not show legacy metrics (utilization, maturity, segment)', () => { const report = generatePostureReport(makePostureResult()); assert.ok(!report.includes('Utilization')); assert.ok(!report.includes('Maturity')); assert.ok(!report.includes('Segment')); }); it('includes scanner findings in collapsed details', () => { const report = generatePostureReport(makePostureResult()); assert.ok(report.includes('
')); assert.ok(report.includes('SET')); assert.ok(report.includes('Unknown key')); }); it('skips scanners with no findings', () => { const report = generatePostureReport(makePostureResult()); // CML has 0 findings, should not appear in details assert.ok(!report.includes('CML')); }); it('handles empty areas', () => { const report = generatePostureReport(makePostureResult({ areas: [], topActions: [] })); assert.ok(report.includes('## Health Assessment')); }); }); // ======================================== // generateDriftReport // ======================================== describe('generateDriftReport', () => { it('returns markdown with baseline info', () => { const report = generateDriftReport(makeDiffResult(), 'my-baseline'); assert.ok(report.includes('## Drift Report')); assert.ok(report.includes('my-baseline')); }); it('shows trend indicator', () => { const report = generateDriftReport(makeDiffResult(), 'default'); assert.ok(report.includes('Improving')); }); it('shows score delta', () => { const report = generateDriftReport(makeDiffResult(), 'default'); assert.ok(report.includes('+15')); }); it('shows new findings table', () => { const report = generateDriftReport(makeDiffResult(), 'default'); assert.ok(report.includes('New Findings')); assert.ok(report.includes('New finding')); }); it('shows resolved findings table', () => { const report = generateDriftReport(makeDiffResult(), 'default'); assert.ok(report.includes('Resolved Findings')); assert.ok(report.includes('Fixed issue')); }); it('shows area changes (only non-zero delta)', () => { const report = generateDriftReport(makeDiffResult(), 'default'); assert.ok(report.includes('Settings')); // Hooks has delta 0, should not appear assert.ok(!report.includes('| Hooks |')); }); }); // ======================================== // generatePluginHealthReport // ======================================== describe('generatePluginHealthReport', () => { it('returns markdown with plugin table', () => { const report = generatePluginHealthReport(makeScanResult(), makePluginResults()); assert.ok(report.includes('## Plugin Health')); assert.ok(report.includes('plugin-a')); assert.ok(report.includes('plugin-b')); }); it('shows grades and counts', () => { const report = generatePluginHealthReport(makeScanResult(), makePluginResults()); assert.ok(report.includes('| 5 |')); assert.ok(report.includes('| 3 |')); }); it('includes per-plugin findings', () => { const report = generatePluginHealthReport(makeScanResult(), makePluginResults()); assert.ok(report.includes('Missing frontmatter')); }); it('handles no plugins', () => { const report = generatePluginHealthReport(makeScanResult(), []); assert.ok(report.includes('No plugins found')); }); it('shows cross-plugin issues', () => { const crossFindings = [ { severity: 'high', title: 'Cross-plugin conflict', description: 'Command name clash' }, ]; const report = generatePluginHealthReport(makeScanResult(crossFindings), makePluginResults()); assert.ok(report.includes('Cross-Plugin Issues')); assert.ok(report.includes('Command name clash')); }); }); // ======================================== // generateFullReport // ======================================== describe('generateFullReport', () => { it('combines all sections', () => { const report = generateFullReport( makePostureResult(), { diff: makeDiffResult(), baselineName: 'default' }, { scanResult: makeScanResult(), pluginResults: makePluginResults() }, ); assert.ok(report.includes('# Config-Audit Report')); assert.ok(report.includes('## Health Assessment')); assert.ok(report.includes('## Drift Report')); assert.ok(report.includes('## Plugin Health')); }); it('skips null sections', () => { const report = generateFullReport(makePostureResult(), null, null); assert.ok(report.includes('## Health Assessment')); assert.ok(!report.includes('## Drift Report')); assert.ok(!report.includes('## Plugin Health')); }); it('handles all null inputs', () => { const report = generateFullReport(null, null, null); assert.ok(report.includes('No data provided')); }); it('stays under 500 lines', () => { const report = generateFullReport(makePostureResult(), null, null); const lineCount = report.split('\n').length; assert.ok(lineCount <= 502); // 500 + truncation notice }); });