config-audit/tests/lib/humanizer.test.mjs
Kjell Tore Guttormsen 239e88cecb fix(acr): on-demand copy for oversized skill-body finding (M-BUG-16)
The skill-listing check emits a third finding for an oversized skill BODY
(v5.11 B7, RAW title "Skill body is large (loads on demand when the skill
runs)"). The body is an ON-DEMAND cost — it loads only when the skill is
invoked, not the always-loaded listing Claude reads every turn. The scanner is
careful to distinguish the two (RAW title + comment + evidence note).

But the humanizer-data SKL.static map had no entry for this title, so it fell
through to SKL._default ("A skill is using more of the listing budget than it
should"). The humanized title therefore claimed a listing-budget cost and
directly contradicted the finding's own humanized evidence ("loads ON DEMAND
only ... NOT every turn like the always-loaded listing") — the same internal
contradiction class as M-BUG-15/M-BUG-14, and the same "new finding type added
without a matching humanizer entry" gap the scanner checklist warns about.

Found by finding-granularity premise-verification of the linkedin-posts scan
before feeding it to the analyze pipeline (the prior session's pass focused on
the GAP findings and did not catch the SKL fall-through).

- humanizer-data.mjs: add SKL.static entry for "Skill body is large (loads on
  demand when the skill runs)" with on-demand-correct title ("A skill's body is
  large (it loads only when that skill runs)"), description, and recommendation.
  No listing-budget language; tier1/tier3 forbidden-word checks pass.
- RED-first tests at both layers: humanizer.test.mjs (humanizeFinding path:
  title is not the listing-budget _default, conveys on-demand body) and
  humanizer-data.test.mjs (static entry exists, on-demand-correct).

RAW envelope unaffected (humanizer bypassed for --raw/--json), frozen v5.0.0
snapshots untouched, default-output fixtures contain no oversized-body skill so
no snapshot regen. Suite 1357->1359/0 (+2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
2026-06-30 13:30:22 +02:00

344 lines
15 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 maps SKL oversized-body finding to an on-demand title, not the listing-budget _default', () => {
// RAW title emitted by skill-listing-scanner for body > threshold (v5.11 B7).
// It is an ON-DEMAND body cost, NOT the always-loaded listing budget — the
// scanner is careful to distinguish them, so the humanized title must not
// regress into "listing budget" language via the SKL _default (M-BUG-16).
const input = makeFinding({
scanner: 'SKL',
severity: 'low',
title: 'Skill body is large (loads on demand when the skill runs)',
description: 'Skill "repo-init" (user) has a body of about 6223 tokens (712 lines), over the ~5000-token guidance for a skill body.',
evidence: 'body_tokens~6223; threshold=5000 tok; skill="repo-init"; source=user - this is the skill BODY which loads ON DEMAND only when the skill is invoked - NOT every turn like the always-loaded listing.',
});
const out = humanizeFinding(input);
assert.doesNotMatch(out.title, /listing budget/i,
`body finding must not be framed as a listing-budget cost, got: ${out.title}`);
assert.notEqual(out.title, 'A skill is using more of the listing budget than it should',
'body finding must have its own mapping, not the SKL _default');
assert.match(out.title, /body|on demand|when (it|the skill) runs/i,
`humanized title must convey the on-demand body cost, 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');
});