config-audit/tests/lib/humanizer.test.mjs
Kjell Tore Guttormsen ba9f82f952 feat(tokens): stale plugin-cache disk-cleanup finding, honestly labelled Dead config (v5.9 B3b)
The B3a cache filter already exposes discovery.staleCacheVersions; surface them
as a finding so the user knows the superseded plugin versions are safe to delete.

Honesty (Verifiseringsplikt): the finding loads on ZERO turns, so it must NOT
read as a per-turn token cost. TOK normally humanizes to "Wasted tokens"; a new
per-finding category override ('plugin-cache-hygiene' -> "Dead config") plus a
dedicated humanizer translation ("Old plugin versions are sitting on disk (safe
to delete) ... cost zero tokens per turn ... housekeeping, not a performance
problem") keep the prose accurate instead of the generic "using more space"
default. evidence carries the explicit "zero live-context impact" note.

- token-hotspots: Pattern H emits CA-TOK (low) from discovery.staleCacheVersions
  when stale versions exist (`--global`); silent otherwise.
- humanizer: CATEGORY_TO_IMPACT lets a finding's category override the
  scanner-default impact label (raw `category` field unchanged -> --json/--raw
  byte-stable). humanizer-data: honest static translation for the finding.
- Tests: finding fires/severity/category, lists stale keys + zero-impact note,
  silent when none; humanizer override -> Dead config; honest translation locked.
- Docs: tokens command render note (disk-cleanup, not a token problem) +
  --no-exclude-cache flag; README + CLAUDE.md rows (7 patterns, cache-aware).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:17:55 +02:00

323 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: stale plugin-cache title humanizes to honest disk-cleanup prose, not the generic TOK default (B3)', () => {
const input = makeFinding({
scanner: 'TOK',
title: 'Stale plugin-cache versions (disk cleanup, zero live-context impact)',
category: 'plugin-cache-hygiene',
});
const out = humanizeFinding(input);
assert.match(out.title, /disk/i, 'humanized title must name disk, not generic "space"');
assert.match(out.description, /zero tokens per turn|never loaded/i,
'humanized description must keep the zero-context-impact honesty');
assert.equal(out.userImpactCategory, 'Dead config');
});
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/OST', () => {
for (const s of ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'PLH', 'OST']) {
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/SKL', () => {
for (const s of ['TOK', 'CPS', 'SKL']) {
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: per-finding category plugin-cache-hygiene overrides scanner default to Dead config (B3)', () => {
// TOK normally maps to "Wasted tokens", but a stale-cache finding loads on
// zero turns — its honest impact is "Dead config", not wasted per-turn tokens.
const out = humanizeFinding(makeFinding({ scanner: 'TOK', category: 'plugin-cache-hygiene' }));
assert.equal(out.userImpactCategory, 'Dead config');
assert.equal(out.category, 'plugin-cache-hygiene', 'raw category field preserved unchanged');
});
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');
});