import { test } from 'node:test'; import assert from 'node:assert/strict'; import { humanizeFinding, humanizeFindings, humanizeEnvelope, computeRelevanceContext, } from '../../scanners/lib/humanizer.mjs'; // ─── helpers ──────────────────────────────────────────────────────────── function makeFinding(overrides = {}) { return { id: 'CA-CML-001', scanner: 'CML', severity: 'medium', title: 'No CLAUDE.md found', description: 'No CLAUDE.md file at the project root.', file: '/Users/test/project/CLAUDE.md', line: null, evidence: 'evidence text', category: 'config', recommendation: 'Create one.', autoFixable: false, ...overrides, }; } // ─── purity ───────────────────────────────────────────────────────────── test('humanizeFinding does not mutate its input', () => { const input = makeFinding(); const before = JSON.parse(JSON.stringify(input)); humanizeFinding(input); assert.deepEqual(input, before, 'input was mutated'); }); test('humanizeFindings does not mutate its input array', () => { const input = [makeFinding(), makeFinding({ id: 'CA-CML-002', title: 'Repeated content detected' })]; const before = JSON.parse(JSON.stringify(input)); humanizeFindings(input); assert.deepEqual(input, before, 'input array was mutated'); }); test('humanizeEnvelope does not mutate its input', () => { const env = { target_path: '/tmp', scanners: [ { scanner: 'CML', status: 'ok', findings: [makeFinding()], counts: {} }, ], }; const before = JSON.parse(JSON.stringify(env)); humanizeEnvelope(env); assert.deepEqual(env, before, 'envelope was mutated'); }); // ─── field preservation ──────────────────────────────────────────────── test('humanizeFinding preserves id, scanner, severity, file, line, evidence, category, autoFixable', () => { const input = makeFinding({ id: 'CA-CML-042', scanner: 'CML', severity: 'high', file: '/tmp/x.md', line: 17, evidence: 'specific snippet', category: 'cml', autoFixable: true, }); const out = humanizeFinding(input); assert.equal(out.id, 'CA-CML-042'); assert.equal(out.scanner, 'CML'); assert.equal(out.severity, 'high'); assert.equal(out.file, '/tmp/x.md'); assert.equal(out.line, 17); assert.equal(out.evidence, 'specific snippet'); assert.equal(out.category, 'cml'); assert.equal(out.autoFixable, true); }); test('humanizeFinding preserves optional details payload', () => { const input = makeFinding(); input.details = { foo: 'bar', count: 7 }; const out = humanizeFinding(input); assert.deepEqual(out.details, { foo: 'bar', count: 7 }); }); // ─── translation lookup ──────────────────────────────────────────────── test('humanizeFinding rewrites title for known static title', () => { const input = makeFinding({ scanner: 'CML', title: 'No CLAUDE.md found' }); const out = humanizeFinding(input); assert.notEqual(out.title, input.title, 'title should be translated'); assert.ok(out.title.toLowerCase().includes('instructions') || out.title.toLowerCase().includes('claude'), `humanized title should mention instructions or claude, got: ${out.title}`); }); test('humanizeFinding falls back to _default when title unknown', () => { const input = makeFinding({ scanner: 'CML', title: 'Unrecognized brand-new finding title' }); const out = humanizeFinding(input); assert.notEqual(out.title, input.title, '_default should kick in'); // CML _default mentions "instructions file" assert.ok(/instructions file/i.test(out.title), `expected CML _default title, got: ${out.title}`); }); test('humanizeFinding passes through original strings when scanner prefix unknown', () => { const input = makeFinding({ scanner: 'XXX', title: 'whatever' }); const out = humanizeFinding(input); assert.equal(out.title, 'whatever'); assert.equal(out.description, input.description); assert.equal(out.recommendation, input.recommendation); }); test('humanizeFinding matches pattern entries (template-literal titles)', () => { const input = makeFinding({ scanner: 'COL', title: 'Skill name "okr-helper" used by multiple plugins', }); const out = humanizeFinding(input); assert.notEqual(out.title, input.title); assert.ok(/two plugins|same name|multiple/i.test(out.title) || /two plugins|same name|multiple/i.test(out.description), `expected pattern match for COL multiple-plugins case, got title: ${out.title}`); }); // ─── userActionLanguage ──────────────────────────────────────────────── test('humanizeFinding maps severity=critical -> "Fix this now"', () => { const out = humanizeFinding(makeFinding({ severity: 'critical' })); assert.equal(out.userActionLanguage, 'Fix this now'); }); test('humanizeFinding maps severity=high -> "Fix soon"', () => { const out = humanizeFinding(makeFinding({ severity: 'high' })); assert.equal(out.userActionLanguage, 'Fix soon'); }); test('humanizeFinding maps severity=medium -> "Fix when convenient"', () => { const out = humanizeFinding(makeFinding({ severity: 'medium' })); assert.equal(out.userActionLanguage, 'Fix when convenient'); }); test('humanizeFinding maps severity=low -> "Optional cleanup"', () => { const out = humanizeFinding(makeFinding({ severity: 'low' })); assert.equal(out.userActionLanguage, 'Optional cleanup'); }); test('humanizeFinding maps severity=info -> "FYI"', () => { const out = humanizeFinding(makeFinding({ severity: 'info' })); assert.equal(out.userActionLanguage, 'FYI'); }); test('humanizeFinding falls back to "FYI" for unknown severity', () => { const out = humanizeFinding(makeFinding({ severity: 'mystery' })); assert.equal(out.userActionLanguage, 'FYI'); }); // ─── userImpactCategory ──────────────────────────────────────────────── test('humanizeFinding sets category Configuration mistake for CML/SET/HKV/RUL/MCP/IMP/PLH', () => { for (const s of ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'PLH']) { const out = humanizeFinding(makeFinding({ scanner: s })); assert.equal(out.userImpactCategory, 'Configuration mistake', `${s} should map to Configuration mistake`); } }); test('humanizeFinding sets category Conflict for CNF/COL', () => { for (const s of ['CNF', 'COL']) { const out = humanizeFinding(makeFinding({ scanner: s })); assert.equal(out.userImpactCategory, 'Conflict'); } }); test('humanizeFinding sets category Wasted tokens for TOK/CPS', () => { for (const s of ['TOK', 'CPS']) { const out = humanizeFinding(makeFinding({ scanner: s })); assert.equal(out.userImpactCategory, 'Wasted tokens'); } }); test('humanizeFinding sets category Dead config for DIS', () => { const out = humanizeFinding(makeFinding({ scanner: 'DIS' })); assert.equal(out.userImpactCategory, 'Dead config'); }); test('humanizeFinding sets category Missed opportunity for GAP', () => { const out = humanizeFinding(makeFinding({ scanner: 'GAP', title: 'No CLAUDE.md file' })); assert.equal(out.userImpactCategory, 'Missed opportunity'); }); test('humanizeFinding sets category Other for unknown scanner', () => { const out = humanizeFinding(makeFinding({ scanner: 'XXX' })); assert.equal(out.userImpactCategory, 'Other'); }); // ─── relevanceContext ────────────────────────────────────────────────── test('computeRelevanceContext detects test-fixture paths', () => { assert.equal(computeRelevanceContext('/repo/tests/fixtures/foo/CLAUDE.md'), 'test-fixture-no-impact'); assert.equal(computeRelevanceContext('/repo/test/fixtures/bar.json'), 'test-fixture-no-impact'); }); test('computeRelevanceContext detects local-only paths via .local. infix', () => { assert.equal(computeRelevanceContext('/repo/.claude/settings.local.json'), 'affects-this-machine-only'); assert.equal(computeRelevanceContext('/repo/CLAUDE.local.md'), 'affects-this-machine-only'); }); test('computeRelevanceContext defaults to affects-everyone for normal paths', () => { assert.equal(computeRelevanceContext('/repo/CLAUDE.md'), 'affects-everyone'); assert.equal(computeRelevanceContext('/repo/.claude/settings.json'), 'affects-everyone'); }); test('computeRelevanceContext defaults to affects-everyone for null/empty paths', () => { assert.equal(computeRelevanceContext(null), 'affects-everyone'); assert.equal(computeRelevanceContext(undefined), 'affects-everyone'); assert.equal(computeRelevanceContext(''), 'affects-everyone'); }); test('humanizeFinding sets relevanceContext from file', () => { const f = makeFinding({ file: '/repo/tests/fixtures/x.json' }); assert.equal(humanizeFinding(f).relevanceContext, 'test-fixture-no-impact'); const g = makeFinding({ file: '/repo/.claude/settings.local.json' }); assert.equal(humanizeFinding(g).relevanceContext, 'affects-this-machine-only'); const h = makeFinding({ file: '/repo/CLAUDE.md' }); assert.equal(humanizeFinding(h).relevanceContext, 'affects-everyone'); }); // ─── humanizeFindings & humanizeEnvelope ────────────────────────────── test('humanizeFindings translates each finding in the array', () => { const findings = [ makeFinding({ scanner: 'CML', title: 'No CLAUDE.md found' }), makeFinding({ id: 'CA-CML-002', scanner: 'CML', title: 'Uses HTML comments' }), ]; const out = humanizeFindings(findings); assert.equal(out.length, 2); assert.notEqual(out[0].title, findings[0].title); assert.notEqual(out[1].title, findings[1].title); }); test('humanizeFindings returns input unchanged if not an array', () => { assert.equal(humanizeFindings(null), null); assert.equal(humanizeFindings(undefined), undefined); }); test('humanizeEnvelope walks scanners[].findings and humanizes each', () => { const env = { target_path: '/tmp', scanners: [ { scanner: 'CML', status: 'ok', findings: [makeFinding({ scanner: 'CML', title: 'No CLAUDE.md found' })], counts: {}, }, { scanner: 'TOK', status: 'ok', findings: [makeFinding({ id: 'CA-TOK-001', scanner: 'TOK', severity: 'low', title: 'Cache-breaking volatile content at top of CLAUDE.md', file: '/tmp/CLAUDE.md', })], counts: {}, }, ], }; const out = humanizeEnvelope(env); assert.equal(out.target_path, '/tmp'); assert.equal(out.scanners.length, 2); assert.notEqual(out.scanners[0].findings[0].title, env.scanners[0].findings[0].title); assert.notEqual(out.scanners[1].findings[0].title, env.scanners[1].findings[0].title); assert.equal(out.scanners[1].findings[0].userImpactCategory, 'Wasted tokens'); }); test('humanizeEnvelope returns input unchanged if shape is wrong', () => { assert.equal(humanizeEnvelope(null), null); assert.equal(humanizeEnvelope({}).scanners, undefined); // unchanged object assert.equal(humanizeEnvelope({ scanners: 'not-an-array' }).scanners, 'not-an-array'); }); // ─── new fields presence ─────────────────────────────────────────────── test('humanizeFinding always sets the three new fields', () => { const out = humanizeFinding(makeFinding()); assert.equal(typeof out.userImpactCategory, 'string'); assert.equal(typeof out.userActionLanguage, 'string'); assert.equal(typeof out.relevanceContext, 'string'); }); // ─── ANSI-free guarantee ─────────────────────────────────────────────── test('humanized output contains no ANSI escape sequences', () => { const out = humanizeFinding(makeFinding({ scanner: 'CML', title: 'No CLAUDE.md found' })); const allText = `${out.title} ${out.description} ${out.recommendation}`; // eslint-disable-next-line no-control-regex assert.equal(/\[/.test(allText), false, 'ANSI escape detected in humanized output'); });