ktg-plugin-marketplace/plugins/config-audit/tests/lib/report-generator.test.mjs

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
});
});