import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { generateHealthScorecard, topActions } from '../../scanners/lib/scoring.mjs'; const SAMPLE_AREA_SCORES = { areas: [ { id: 'claude_md', name: 'CLAUDE.md', grade: 'A', score: 100, findingCount: 0 }, { id: 'settings', name: 'Settings', grade: 'A', score: 90, findingCount: 1 }, { id: 'hooks', name: 'Hooks', grade: 'A', score: 100, findingCount: 0 }, { id: 'feature_coverage', name: 'Feature Coverage', grade: 'D', score: 30, findingCount: 17 }, ], overallGrade: 'A', }; const SAMPLE_GAP_FINDINGS = [ { id: 'CA-GAP-001', scanner: 'GAP', severity: 'medium', title: 'No CLAUDE.md file', description: 'No project instructions file detected.', recommendation: 'Create a CLAUDE.md file with project-specific guidance.', category: 't1', file: null, }, { id: 'CA-GAP-002', scanner: 'GAP', severity: 'medium', title: 'No permissions configured', description: 'No permissions block in settings.', recommendation: 'Add a permissions block to settings.json.', category: 't1', file: null, }, { id: 'CA-GAP-003', scanner: 'GAP', severity: 'low', title: 'No status line configured', description: 'No status line.', recommendation: 'Add a status line.', category: 't3', file: null, }, ]; describe('generateHealthScorecard signature change (3-param)', () => { it('2-arg call: backwards-compatible (humanized defaults to false)', () => { const out = generateHealthScorecard(SAMPLE_AREA_SCORES, 17); assert.equal(typeof out, 'string'); assert.ok(out.length > 0); assert.ok(out.includes('Config-Audit Health Score'), 'non-humanized scorecard should contain v5.0.0 title'); }); it('3-arg call with {humanized: false}: byte-equal to 2-arg call', () => { const twoArg = generateHealthScorecard(SAMPLE_AREA_SCORES, 17); const threeArgFalse = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: false }); assert.equal(threeArgFalse, twoArg, 'options.humanized=false must produce identical output to 2-arg call'); }); it('3-arg call with {humanized: true}: differs from non-humanized', () => { const nonHumanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: false }); const humanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: true }); assert.notEqual(humanized, nonHumanized, 'humanized=true must produce different output from humanized=false'); }); it('3-arg call with {humanized: true}: contains user-friendly phrasing', () => { const humanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: true }); // Must contain at least one humanized cue distinguishing it from v5.0.0 prose const hasGradeContext = /healthy|good shape|attention|polish|setup/i.test(humanized); assert.ok(hasGradeContext, `humanized scorecard must include user-friendly grade context, got:\n${humanized}`); }); it('preserves area names and scores in both modes', () => { const nonHumanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: false }); const humanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: true }); for (const area of SAMPLE_AREA_SCORES.areas.filter(a => a.name !== 'Feature Coverage')) { assert.ok(nonHumanized.includes(area.name), `non-humanized scorecard must include area name "${area.name}"`); assert.ok(humanized.includes(area.name), `humanized scorecard must include area name "${area.name}"`); assert.ok(nonHumanized.includes(`(${area.score})`), `non-humanized scorecard must include score (${area.score})`); assert.ok(humanized.includes(`(${area.score})`), `humanized scorecard must include score (${area.score})`); } }); it('opportunity count handling in humanized mode', () => { const humanizedZero = generateHealthScorecard(SAMPLE_AREA_SCORES, 0, { humanized: true }); const humanizedMany = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: true }); assert.ok(humanizedMany.includes('17'), 'humanized scorecard must include opportunity count'); // Both paths must remain finite strings assert.equal(typeof humanizedZero, 'string'); assert.equal(typeof humanizedMany, 'string'); }); }); describe('topActions humanizer support', () => { it('1-arg call: returns raw recommendations (backwards-compatible)', () => { const actions = topActions(SAMPLE_GAP_FINDINGS); assert.equal(actions.length, 3); assert.equal(actions[0], 'Create a CLAUDE.md file with project-specific guidance.'); assert.equal(actions[1], 'Add a permissions block to settings.json.'); assert.equal(actions[2], 'Add a status line.'); }); it('2-arg call with {humanized: false}: identical to 1-arg call', () => { const oneArg = topActions(SAMPLE_GAP_FINDINGS); const twoArg = topActions(SAMPLE_GAP_FINDINGS, { humanized: false }); assert.deepStrictEqual(twoArg, oneArg); }); it('2-arg call with {humanized: true}: at least one recommendation differs', () => { const raw = topActions(SAMPLE_GAP_FINDINGS, { humanized: false }); const humanized = topActions(SAMPLE_GAP_FINDINGS, { humanized: true }); assert.equal(humanized.length, raw.length, 'array length preserved'); // The humanizer's GAP TRANSLATIONS replace at least one recommendation (No CLAUDE.md → "Add the file…") const anyDiffer = humanized.some((r, i) => r !== raw[i]); assert.ok(anyDiffer, `humanized=true must change at least one recommendation. raw=${JSON.stringify(raw)} humanized=${JSON.stringify(humanized)}`); }); it('preserves ordering by tier (t1 → t2 → t3)', () => { const humanized = topActions(SAMPLE_GAP_FINDINGS, { humanized: true }); assert.equal(humanized.length, 3); // 1st & 2nd: t1 findings, 3rd: t3 finding (t2 absent in sample) // Both modes preserve this ordering. }); });