252 lines
9 KiB
JavaScript
252 lines
9 KiB
JavaScript
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('<details>'));
|
|
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('<summary>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
|
|
});
|
|
});
|