import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { finding, scannerResult, envelope } from '../../scanners/lib/output.mjs'; describe('finding', () => { it('generates correct ID format', () => { const f = finding({ scanner: 'CML', code: 'no-claude-md', severity: 'high', title: 't', description: 'd' }); assert.match(f.id, /^CA-CML-\d{3}$/); }); it('gives a check the same ID however many findings precede it', () => { // The ID names the check, not the emission (M-BUG-28). Emitting other // findings first must not move it. const first = finding({ scanner: 'CML', code: 'todo-markers', severity: 'info', title: 'a', description: 'b' }); finding({ scanner: 'CML', code: 'no-headings', severity: 'info', title: 'c', description: 'd' }); finding({ scanner: 'CML', code: 'html-comments', severity: 'info', title: 'e', description: 'f' }); const again = finding({ scanner: 'CML', code: 'todo-markers', severity: 'info', title: 'a', description: 'b' }); assert.strictEqual(first.id, again.id); }); it('refuses to invent an ID when the check is not named', () => { assert.throws( () => finding({ scanner: 'CML', severity: 'info', title: 'a', description: 'b' }), /missing "code"/, ); }); it('includes all required fields', () => { const f = finding({ scanner: 'SET', code: 'invalid-json', severity: 'critical', title: 'Test', description: 'Desc', file: '/foo.json', line: 10, evidence: 'x=1', category: 'Structure', recommendation: 'Fix it', autoFixable: true, }); assert.strictEqual(f.scanner, 'SET'); assert.strictEqual(f.severity, 'critical'); assert.strictEqual(f.title, 'Test'); assert.strictEqual(f.description, 'Desc'); assert.strictEqual(f.file, '/foo.json'); assert.strictEqual(f.line, 10); assert.strictEqual(f.evidence, 'x=1'); assert.strictEqual(f.category, 'Structure'); assert.strictEqual(f.recommendation, 'Fix it'); assert.strictEqual(f.autoFixable, true); }); it('defaults nullable fields to null', () => { const f = finding({ scanner: 'SET', code: 'missing-schema', severity: 'info', title: 't', description: 'd' }); assert.strictEqual(f.file, null); assert.strictEqual(f.line, null); assert.strictEqual(f.evidence, null); assert.strictEqual(f.category, null); assert.strictEqual(f.recommendation, null); assert.strictEqual(f.autoFixable, false); }); }); describe('scannerResult', () => { it('counts severity correctly', () => { const findings = [ finding({ scanner: 'SET', code: 'invalid-json', severity: 'critical', title: 'a', description: 'b' }), finding({ scanner: 'SET', code: 'key-typo', severity: 'high', title: 'c', description: 'd' }), finding({ scanner: 'SET', code: 'deprecated-key', severity: 'high', title: 'e', description: 'f' }), finding({ scanner: 'SET', code: 'missing-schema', severity: 'info', title: 'g', description: 'h' }), ]; const r = scannerResult('SET', 'ok', findings, 5, 100); assert.strictEqual(r.counts.critical, 1); assert.strictEqual(r.counts.high, 2); assert.strictEqual(r.counts.medium, 0); assert.strictEqual(r.counts.low, 0); assert.strictEqual(r.counts.info, 1); }); it('includes error message when provided', () => { const r = scannerResult('SET', 'error', [], 0, 50, 'boom'); assert.strictEqual(r.error, 'boom'); }); it('omits error when not provided', () => { const r = scannerResult('SET', 'ok', [], 3, 100); assert.strictEqual(r.error, undefined); }); it('returns correct structure', () => { const r = scannerResult('CML', 'ok', [], 2, 42); assert.strictEqual(r.scanner, 'CML'); assert.strictEqual(r.status, 'ok'); assert.strictEqual(r.files_scanned, 2); assert.strictEqual(r.duration_ms, 42); assert.deepStrictEqual(r.findings, []); }); }); describe('envelope', () => { it('aggregates across scanners', () => { const r1 = scannerResult('CML', 'ok', [ finding({ scanner: 'CML', code: 'no-claude-md', severity: 'high', title: 'x', description: 'y' }), ], 1, 10); const r2 = scannerResult('SET', 'ok', [ finding({ scanner: 'SET', code: 'invalid-json', severity: 'critical', title: 'a', description: 'b' }), finding({ scanner: 'SET', code: 'no-deny-rules', severity: 'low', title: 'c', description: 'd' }), ], 2, 20); const env = envelope('/target', [r1, r2], 50); assert.strictEqual(env.aggregate.total_findings, 3); assert.strictEqual(env.aggregate.counts.critical, 1); assert.strictEqual(env.aggregate.counts.high, 1); assert.strictEqual(env.aggregate.counts.low, 1); assert.strictEqual(env.aggregate.scanners_ok, 2); }); it('counts scanner statuses', () => { const r1 = scannerResult('A', 'ok', [], 1, 10); const r2 = scannerResult('B', 'skipped', [], 0, 5); const r3 = scannerResult('C', 'error', [], 0, 3, 'fail'); const env = envelope('/t', [r1, r2, r3], 30); assert.strictEqual(env.aggregate.scanners_ok, 1); assert.strictEqual(env.aggregate.scanners_skipped, 1); assert.strictEqual(env.aggregate.scanners_error, 1); }); it('includes meta with version and tool', () => { const env = envelope('/t', [], 0); assert.strictEqual(env.meta.version, '2.2.0'); assert.strictEqual(env.meta.tool, 'config-audit'); assert.strictEqual(env.meta.target, '/t'); assert.ok(env.meta.timestamp); }); it('calculates verdict correctly', () => { const env = envelope('/t', [], 0); assert.strictEqual(env.aggregate.verdict, 'PASS'); }); });