Fase 4 Items 2+3 (CC 2.1.114→181 gap-review). New orchestrated scanner `skill-listing-scanner.mjs` (prefix SKL) flags every active skill whose description exceeds the verified 1,536-char listing cap (CC 2.1.105, changelog L1502). Past the cap, Claude Code silently truncates the description the model reads to route skill invocation — dropping the trigger phrases at the tail. HOME-scoped over all user + plugin skills via enumerateSkills (COL is the model). - CA-SKL-001 (medium): description > 1,536 chars. Remediation folds in Item 2(b) — recommends disableBundledSkills + skillOverrides + trimming (designvalg A: no standalone GAP-check, which would fire for nearly everyone). - Designvalg B: v1 ships the verified cap ONLY. The aggregate 2%-of-context listing budget is deferred — it needs a context-window assumption that would turn a verified fact into a guess (would carry a CALIBRATION_NOTE if added). - Choice C: recognize the skillOverrides settings key (CC 2.1.129) in KNOWN_KEYS. Left OUT of TYPE_CHECKS — the value is a per-skill object (off/user-invocable-only/name-only), not a string; a 'string' check (as the plan sketched) would create a NEW false positive. Verify-first deviation. Registration: scan-orchestrator (13th scanner), humanizer (SKL → 'Wasted tokens' + static/_default translations), scoring SCANNER_AREA_MAP (→ Token Efficiency; no 11th area), README badge 12→13, CLAUDE.md (finding-id + test-count), docs/scanner-internals.md, gap-matrix + plan status notes. Snapshots reseeded hermetically (SEED_SNAPSHOT/UPDATE_SNAPSHOT): SKL entry with 0 findings in empty HOME, scanners_ok 11→12, claudeMdEstimatedTokens bump from the CLAUDE.md edits flowing through the cascade. Contamination grep clean. Suite 868/868 (856 baseline + 11 SKL + 1 skillOverrides). RED→GREEN logged per cycle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
153 lines
6.2 KiB
JavaScript
153 lines
6.2 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { resolve, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
import { readFile, unlink } from 'node:fs/promises';
|
|
import { hermeticEnv } from '../helpers/hermetic-home.mjs';
|
|
|
|
const exec = promisify(execFile);
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const REPO = resolve(__dirname, '../..');
|
|
const CLI = resolve(REPO, 'scanners/scan-orchestrator.mjs');
|
|
const FIXTURE = resolve(REPO, 'tests/fixtures/marketplace-medium');
|
|
const SNAPSHOT_PATH = resolve(REPO, 'tests/snapshots/v5.0.0/scan-orchestrator.json');
|
|
|
|
/**
|
|
* Normalize a scan-orchestrator envelope for snapshot comparison by
|
|
* blanking out time-varying fields (timestamp, durations, target path)
|
|
* and ancestor-cascade-derived counts. `claudeMdEstimatedTokens` reflects
|
|
* walkClaudeMdCascade walking upward from the fixture; any docs edit to
|
|
* this plugin's own CLAUDE.md ripples into it even when scanner behavior
|
|
* is unchanged. Returns a NEW object — does not mutate input.
|
|
*/
|
|
function normalizeEnvelope(env) {
|
|
const out = JSON.parse(JSON.stringify(env));
|
|
if (out.meta) {
|
|
out.meta.target = '<TARGET>';
|
|
out.meta.timestamp = '<TIMESTAMP>';
|
|
}
|
|
if (Array.isArray(out.scanners)) {
|
|
for (const s of out.scanners) {
|
|
s.duration_ms = 0;
|
|
if (s.activeConfig && 'claudeMdEstimatedTokens' in s.activeConfig) {
|
|
s.activeConfig.claudeMdEstimatedTokens = '<ANCESTOR_DERIVED>';
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
async function runOrchestrator(flags) {
|
|
const out = `/tmp/scan-orch-humanizer-${process.pid}-${Date.now()}-${Math.random()}.json`;
|
|
try {
|
|
await exec('node', [CLI, FIXTURE, '--output-file', out, ...flags], {
|
|
timeout: 60000,
|
|
cwd: REPO,
|
|
env: hermeticEnv(),
|
|
});
|
|
const written = await readFile(out, 'utf-8');
|
|
return JSON.parse(written);
|
|
} finally {
|
|
await unlink(out).catch(() => {});
|
|
}
|
|
}
|
|
|
|
describe('scan-orchestrator humanizer wiring (Step 5)', () => {
|
|
describe('--json mode (SC-6: byte-equal v5.0.0)', () => {
|
|
it('produces envelope structurally equal to v5.0.0 snapshot', async () => {
|
|
const actual = await runOrchestrator(['--json']);
|
|
const expected = JSON.parse(await readFile(SNAPSHOT_PATH, 'utf-8'));
|
|
assert.deepStrictEqual(normalizeEnvelope(actual), normalizeEnvelope(expected));
|
|
});
|
|
|
|
it('does NOT add humanizer fields to findings', async () => {
|
|
const actual = await runOrchestrator(['--json']);
|
|
for (const s of actual.scanners) {
|
|
for (const f of s.findings) {
|
|
assert.equal(f.userImpactCategory, undefined,
|
|
`${f.id}: --json findings must not have userImpactCategory`);
|
|
assert.equal(f.userActionLanguage, undefined,
|
|
`${f.id}: --json findings must not have userActionLanguage`);
|
|
assert.equal(f.relevanceContext, undefined,
|
|
`${f.id}: --json findings must not have relevanceContext`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('--raw mode (SC-7: byte-equal v5.0.0)', () => {
|
|
it('produces envelope structurally equal to v5.0.0 snapshot', async () => {
|
|
const actual = await runOrchestrator(['--raw']);
|
|
const expected = JSON.parse(await readFile(SNAPSHOT_PATH, 'utf-8'));
|
|
assert.deepStrictEqual(normalizeEnvelope(actual), normalizeEnvelope(expected));
|
|
});
|
|
|
|
it('does NOT add humanizer fields to findings', async () => {
|
|
const actual = await runOrchestrator(['--raw']);
|
|
for (const s of actual.scanners) {
|
|
for (const f of s.findings) {
|
|
assert.equal(f.userImpactCategory, undefined,
|
|
`${f.id}: --raw findings must not have userImpactCategory`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('default mode (humanized)', () => {
|
|
it('preserves envelope-level shape', async () => {
|
|
const actual = await runOrchestrator([]);
|
|
assert.ok(actual.meta, 'meta present');
|
|
assert.ok(Array.isArray(actual.scanners), 'scanners array present');
|
|
assert.ok(actual.aggregate, 'aggregate present');
|
|
assert.equal(actual.scanners.length, 13, 'all 13 scanners present');
|
|
});
|
|
|
|
it('preserves scanner shape (scanner/status/findings/counts)', async () => {
|
|
const actual = await runOrchestrator([]);
|
|
for (const s of actual.scanners) {
|
|
assert.ok(typeof s.scanner === 'string', 'scanner name string');
|
|
assert.ok(typeof s.status === 'string', 'status string');
|
|
assert.ok(Array.isArray(s.findings), 'findings array');
|
|
assert.ok(s.counts, 'counts object');
|
|
}
|
|
});
|
|
|
|
it('adds humanizer fields to every finding', async () => {
|
|
const actual = await runOrchestrator([]);
|
|
let totalFindings = 0;
|
|
for (const s of actual.scanners) {
|
|
for (const f of s.findings) {
|
|
totalFindings++;
|
|
assert.equal(typeof f.userImpactCategory, 'string',
|
|
`${f.id}: userImpactCategory must be string`);
|
|
assert.equal(typeof f.userActionLanguage, 'string',
|
|
`${f.id}: userActionLanguage must be string`);
|
|
assert.equal(typeof f.relevanceContext, 'string',
|
|
`${f.id}: relevanceContext must be string`);
|
|
assert.ok(['test-fixture-no-impact', 'affects-this-machine-only', 'affects-everyone'].includes(f.relevanceContext),
|
|
`${f.id}: relevanceContext must be one of allowed values, got ${f.relevanceContext}`);
|
|
}
|
|
}
|
|
assert.ok(totalFindings > 0, 'expected at least one finding to assert against');
|
|
});
|
|
|
|
it('preserves stable identifiers (id, scanner, severity)', async () => {
|
|
const actualHumanized = await runOrchestrator([]);
|
|
const actualRaw = await runOrchestrator(['--raw']);
|
|
|
|
const flatHumanized = actualHumanized.scanners.flatMap(s => s.findings);
|
|
const flatRaw = actualRaw.scanners.flatMap(s => s.findings);
|
|
assert.equal(flatHumanized.length, flatRaw.length, 'finding count matches');
|
|
|
|
for (let i = 0; i < flatHumanized.length; i++) {
|
|
const h = flatHumanized[i];
|
|
const r = flatRaw[i];
|
|
assert.equal(h.id, r.id, `finding ${i} id matches`);
|
|
assert.equal(h.scanner, r.scanner, `finding ${i} scanner matches`);
|
|
assert.equal(h.severity, r.severity, `finding ${i} severity matches`);
|
|
}
|
|
});
|
|
});
|
|
});
|