import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { diffEnvelopes, formatDiffReport } from '../../scanners/lib/diff-engine.mjs'; // --- Helpers --- function makeFinding(scanner, title, severity = 'medium', file = null) { return { id: `CA-${scanner}-001`, scanner, severity, title, description: `Description for ${title}`, file, line: null, evidence: null, category: null, recommendation: null, autoFixable: false, }; } function makeScannerResult(scanner, findings) { const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 }; for (const f of findings) { if (counts[f.severity] !== undefined) counts[f.severity]++; } return { scanner, status: 'ok', files_scanned: 3, duration_ms: 10, findings, counts, }; } function makeEnvelope(scannerResults) { const aggregate = { critical: 0, high: 0, medium: 0, low: 0, info: 0 }; let total = 0; for (const r of scannerResults) { for (const sev of Object.keys(aggregate)) { aggregate[sev] += (r.counts[sev] || 0); } total += r.findings.length; } return { meta: { target: '/test', timestamp: new Date().toISOString(), version: '2.0.0', tool: 'config-audit' }, scanners: scannerResults, aggregate: { total_findings: total, counts: aggregate, risk_score: 0, risk_band: 'Low', verdict: 'PASS', scanners_ok: scannerResults.length, scanners_error: 0, scanners_skipped: 0 }, }; } // ======================================== // diffEnvelopes // ======================================== describe('diffEnvelopes', () => { it('identifies new findings', () => { const baseline = makeEnvelope([makeScannerResult('CML', [])]); const current = makeEnvelope([ makeScannerResult('CML', [makeFinding('CML', 'New issue', 'high', 'CLAUDE.md')]), ]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.newFindings.length, 1); assert.equal(diff.newFindings[0].title, 'New issue'); assert.equal(diff.resolvedFindings.length, 0); }); it('identifies resolved findings', () => { const baseline = makeEnvelope([ makeScannerResult('CML', [makeFinding('CML', 'Old issue', 'high', 'CLAUDE.md')]), ]); const current = makeEnvelope([makeScannerResult('CML', [])]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.resolvedFindings.length, 1); assert.equal(diff.resolvedFindings[0].title, 'Old issue'); assert.equal(diff.newFindings.length, 0); }); it('identifies unchanged findings', () => { const f = makeFinding('CML', 'Persistent issue', 'medium', 'CLAUDE.md'); const baseline = makeEnvelope([makeScannerResult('CML', [f])]); const current = makeEnvelope([makeScannerResult('CML', [{ ...f }])]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.unchangedFindings.length, 1); assert.equal(diff.newFindings.length, 0); assert.equal(diff.resolvedFindings.length, 0); }); it('detects moved findings (same title, different file)', () => { const baseFinding = makeFinding('CML', 'Moved issue', 'high', 'old-file.md'); const currFinding = makeFinding('CML', 'Moved issue', 'high', 'new-file.md'); const baseline = makeEnvelope([makeScannerResult('CML', [baseFinding])]); const current = makeEnvelope([makeScannerResult('CML', [currFinding])]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.movedFindings.length, 1); assert.equal(diff.movedFindings[0].from.file, 'old-file.md'); assert.equal(diff.movedFindings[0].to.file, 'new-file.md'); assert.equal(diff.newFindings.length, 0); assert.equal(diff.resolvedFindings.length, 0); }); it('calculates score delta', () => { const baseline = makeEnvelope([ makeScannerResult('CML', [ makeFinding('CML', 'A', 'high', 'a.md'), makeFinding('CML', 'B', 'high', 'a.md'), makeFinding('CML', 'C', 'high', 'a.md'), ]), ]); const current = makeEnvelope([makeScannerResult('CML', [])]); const diff = diffEnvelopes(baseline, current); assert.ok(diff.scoreChange.delta > 0, 'Score should improve when findings are resolved'); assert.equal(typeof diff.scoreChange.before.grade, 'string'); assert.equal(typeof diff.scoreChange.after.grade, 'string'); }); it('calculates area changes', () => { const baseline = makeEnvelope([ makeScannerResult('CML', [makeFinding('CML', 'X', 'low', 'a.md')]), makeScannerResult('SET', []), ]); const current = makeEnvelope([ makeScannerResult('CML', []), makeScannerResult('SET', [makeFinding('SET', 'Y', 'low', 'b.json')]), ]); const diff = diffEnvelopes(baseline, current); assert.ok(diff.areaChanges.length >= 2); const cmlChange = diff.areaChanges.find(a => a.name === 'CLAUDE.md'); assert.ok(cmlChange, 'Should have CLAUDE.md area change'); assert.ok(cmlChange.delta > 0, 'CLAUDE.md should improve'); }); it('detects improving trend', () => { const baseline = makeEnvelope([ makeScannerResult('CML', [ makeFinding('CML', 'A', 'high', 'a.md'), makeFinding('CML', 'B', 'high', 'a.md'), ]), ]); const current = makeEnvelope([makeScannerResult('CML', [])]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.summary.trend, 'improving'); }); it('detects degrading trend', () => { const baseline = makeEnvelope([makeScannerResult('CML', [])]); const current = makeEnvelope([ makeScannerResult('CML', [ makeFinding('CML', 'A', 'high', 'a.md'), makeFinding('CML', 'B', 'high', 'a.md'), ]), ]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.summary.trend, 'degrading'); }); it('detects stable trend (same findings)', () => { const f = makeFinding('CML', 'Same', 'medium', 'a.md'); const baseline = makeEnvelope([makeScannerResult('CML', [f])]); const current = makeEnvelope([makeScannerResult('CML', [{ ...f }])]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.summary.trend, 'stable'); }); it('handles empty baseline', () => { const baseline = makeEnvelope([]); const current = makeEnvelope([ makeScannerResult('CML', [makeFinding('CML', 'New', 'low', 'a.md')]), ]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.newFindings.length, 1); assert.equal(diff.resolvedFindings.length, 0); assert.equal(diff.summary.totalBefore, 0); }); it('handles identical envelopes (all unchanged)', () => { const f1 = makeFinding('CML', 'Issue A', 'medium', 'a.md'); const f2 = makeFinding('SET', 'Issue B', 'low', 'b.json'); const baseline = makeEnvelope([ makeScannerResult('CML', [f1]), makeScannerResult('SET', [f2]), ]); const current = makeEnvelope([ makeScannerResult('CML', [{ ...f1 }]), makeScannerResult('SET', [{ ...f2 }]), ]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.unchangedFindings.length, 2); assert.equal(diff.newFindings.length, 0); assert.equal(diff.resolvedFindings.length, 0); assert.equal(diff.movedFindings.length, 0); assert.equal(diff.summary.trend, 'stable'); }); it('summary has correct counts', () => { const baseline = makeEnvelope([ makeScannerResult('CML', [ makeFinding('CML', 'Keep', 'low', 'a.md'), makeFinding('CML', 'Resolve', 'high', 'b.md'), ]), ]); const current = makeEnvelope([ makeScannerResult('CML', [ makeFinding('CML', 'Keep', 'low', 'a.md'), makeFinding('CML', 'Brand new', 'medium', 'c.md'), ]), ]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.summary.totalBefore, 2); assert.equal(diff.summary.totalAfter, 2); assert.equal(diff.summary.newCount, 1); assert.equal(diff.summary.resolvedCount, 1); }); it('handles findings with null file gracefully', () => { const f = makeFinding('CML', 'No file', 'info', null); const baseline = makeEnvelope([makeScannerResult('CML', [f])]); const current = makeEnvelope([makeScannerResult('CML', [{ ...f }])]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.unchangedFindings.length, 1); }); it('multiple findings with same key are matched correctly', () => { const f1 = makeFinding('CML', 'Duplicate', 'low', 'a.md'); const f2 = makeFinding('CML', 'Duplicate', 'low', 'a.md'); const baseline = makeEnvelope([makeScannerResult('CML', [f1, f2])]); const current = makeEnvelope([makeScannerResult('CML', [{ ...f1 }])]); const diff = diffEnvelopes(baseline, current); assert.equal(diff.unchangedFindings.length, 1); assert.equal(diff.resolvedFindings.length, 1); }); }); // ======================================== // formatDiffReport // ======================================== describe('formatDiffReport', () => { it('returns a non-empty string', () => { const diff = diffEnvelopes(makeEnvelope([]), makeEnvelope([])); const report = formatDiffReport(diff); assert.ok(report.length > 0); assert.equal(typeof report, 'string'); }); it('contains header', () => { const diff = diffEnvelopes(makeEnvelope([]), makeEnvelope([])); const report = formatDiffReport(diff); assert.ok(report.includes('Config-Audit Drift Report')); }); it('shows trend', () => { const baseline = makeEnvelope([ makeScannerResult('CML', [makeFinding('CML', 'X', 'high', 'a.md')]), ]); const current = makeEnvelope([makeScannerResult('CML', [])]); const diff = diffEnvelopes(baseline, current); const report = formatDiffReport(diff); assert.ok(report.includes('Improving')); }); it('lists new findings', () => { const baseline = makeEnvelope([makeScannerResult('CML', [])]); const current = makeEnvelope([ makeScannerResult('CML', [makeFinding('CML', 'Fresh issue', 'high', 'x.md')]), ]); const diff = diffEnvelopes(baseline, current); const report = formatDiffReport(diff); assert.ok(report.includes('Fresh issue')); assert.ok(report.includes('New findings')); }); });