config-audit/tests/scanners/cache-prefix.test.mjs
Kjell Tore Guttormsen dd9db60fc9 fix(acr): CPS ignores fenced/inline code + CC-stable path vars (M-BUG-7)
CPS flagged ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR} (CC-provided stable
paths) and {date}/timestamp tokens shown in documentation as cache-busters.
Fix: skip fenced code blocks, strip inline-code spans, and whitelist CC-stable
vars before pattern-matching. Suppress-only — frozen v5.0.0 snapshots untouched
(CPS yields findings:[] there), no re-seed. Suite 1316/0 (+6). Dogfood ~/.claude
5->2 (3 doc false-positives suppressed; 2 remaining = own volatile test fixtures).
2026-06-26 12:45:14 +02:00

164 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resetCounter } from '../../scanners/lib/output.mjs';
import { scan } from '../../scanners/cache-prefix-scanner.mjs';
import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const FIXTURES = resolve(__dirname, '../fixtures');
async function runScanner(fixtureName) {
resetCounter();
const path = resolve(FIXTURES, fixtureName);
const discovery = await discoverConfigFiles(path);
return scan(path, discovery);
}
describe('CPS scanner — basic structure', () => {
it('reports scanner prefix CPS', async () => {
const result = await runScanner('volatile-mid-section/volatile-line-60');
assert.equal(result.scanner, 'CPS');
});
it('finding IDs match CA-CPS-NNN pattern', async () => {
const result = await runScanner('volatile-mid-section/volatile-line-60');
for (const f of result.findings) {
assert.match(f.id, /^CA-CPS-\d{3}$/);
}
});
});
describe('CPS scanner — volatile content within cached prefix', () => {
it('flags !git log at line 60 (medium severity)', async () => {
const result = await runScanner('volatile-mid-section/volatile-line-60');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.ok(f, `expected volatile-prefix finding; got: ${result.findings.map(x => x.title).join(' | ')}`);
assert.equal(f.severity, 'medium', `expected medium, got ${f.severity}`);
assert.match(String(f.evidence || ''), /line 60/);
assert.match(String(f.evidence || ''), /shell-exec/i);
});
});
describe('CPS scanner — volatile content beyond cache window', () => {
it('does NOT flag volatility at line 200+ (outside 150-line window)', async () => {
const result = await runScanner('volatile-mid-section/volatile-line-200');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.equal(f, undefined,
`expected no finding for line-200 fixture; got: ${f?.title}`);
});
});
describe('CPS scanner — does not duplicate TOK Pattern A territory', () => {
it('volatility at lines 130 is left for TOK Pattern A (no CPS finding)', async () => {
// The opus-47/cache-breaking fixture has volatile content at the very top.
// CPS skips lines 130 to avoid duplicating Pattern A's territory.
const result = await runScanner('opus-47/cache-breaking');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.equal(f, undefined,
`expected no CPS finding when volatility is only in lines 130 (Pattern A's range)`);
});
});
describe('CPS scanner — volatile content in @imported files (v5.10 B6)', () => {
it('flags volatile content inside an @imported file even when the root file is stable', async () => {
const result = await runScanner('import-volatile/positive');
const f = result.findings.find(x => /volatile content in @imported file/i.test(x.title || ''));
assert.ok(f, `expected @import-volatile finding; got: ${result.findings.map(x => x.title).join(' | ')}`);
assert.equal(f.severity, 'medium', `expected medium, got ${f.severity}`);
assert.match(String(f.evidence || ''), /imported by/i);
assert.match(String(f.evidence || ''), /BUILD_TIMESTAMP|git log|VAR substitution|shell-exec/i);
assert.equal(f.category, 'token-efficiency');
});
it('does NOT emit the in-file CPS finding when the importing file itself is stable', async () => {
// Root CLAUDE.md carries no volatile lines; only the @imported file does.
const result = await runScanner('import-volatile/positive');
const inFile = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.equal(inFile, undefined, 'a byte-stable importing file must not trip the in-file finding');
});
it('does NOT flag a clean @imported file', async () => {
const result = await runScanner('import-volatile/clean');
const f = result.findings.find(x => /volatile content in @imported file/i.test(x.title || ''));
assert.equal(f, undefined, `expected no finding for a clean import; got: ${f?.title}`);
});
});
describe('CPS scanner — CC-stable vars are not cache-busters (M-BUG-7)', () => {
it('does NOT flag ${CLAUDE_PLUGIN_ROOT} / ${CLAUDE_PROJECT_DIR} in prose', async () => {
// These are CC-provided path substitutions that resolve to a stable value
// every turn — referencing them in CLAUDE.md prose is not a cache-buster.
const result = await runScanner('cps-stable-vars/whitelisted');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.equal(f, undefined,
`CC-stable path vars must not trip CPS; got: ${f?.evidence}`);
});
it('STILL flags a genuine non-CC ${VAR} substitution (whitelist is selective)', async () => {
const result = await runScanner('cps-stable-vars/non-whitelisted');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.ok(f, `expected a finding for a genuine runtime var; got none`);
assert.match(String(f.evidence || ''), /line 40/);
assert.match(String(f.evidence || ''), /\$\{VAR\} substitution/i);
});
});
describe('CPS scanner — volatility inside fenced code blocks is documentation (M-BUG-7)', () => {
it('does NOT flag volatile-looking lines inside a ```fence```', async () => {
// Content inside a fenced code block is illustrative, byte-stable literal
// text — not runtime volatility — so it must not break the cached prefix.
const result = await runScanner('cps-fenced/inside-fence');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.equal(f, undefined,
`fenced code content must not trip CPS; got: ${f?.evidence}`);
});
it('STILL flags genuine volatility in live prose outside the fence', async () => {
const result = await runScanner('cps-fenced/mixed');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.ok(f, `expected a finding for the out-of-fence volatile line; got none`);
assert.match(String(f.evidence || ''), /line 50/);
assert.match(String(f.evidence || ''), /shell-exec/i);
assert.doesNotMatch(String(f.evidence || ''), /line 3[567]/,
'fenced lines 3537 must not appear in the evidence');
});
});
describe('CPS scanner — volatility inside inline code is documentation (M-BUG-7)', () => {
it('does NOT flag a {date} placeholder shown inside `inline code`', async () => {
// A filename template like `.claude/plans/run-{date}.md` in backticks is
// literal documentation text — byte-stable, not a runtime substitution.
const result = await runScanner('cps-inline-code/pure');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.equal(f, undefined,
`inline-code documentation must not trip CPS; got: ${f?.evidence}`);
});
it('STILL flags a live ${VAR} that sits outside backticks', async () => {
const result = await runScanner('cps-inline-code/mixed');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.ok(f, `expected a finding for the out-of-backtick volatile line; got none`);
assert.match(String(f.evidence || ''), /line 50/);
assert.doesNotMatch(String(f.evidence || ''), /line 40/,
'the backticked {date} on line 40 must not appear in the evidence');
});
});
describe('CPS scanner — orchestrator wiring', () => {
it('CPS appears in scan-orchestrator scanner list', async () => {
const orch = await import('../../scanners/scan-orchestrator.mjs');
const path = resolve(FIXTURES, 'volatile-mid-section/volatile-line-60');
const env = await orch.runAllScanners(path, { filterFixtures: false });
const cps = env.scanners.find(r => r.scanner === 'CPS');
assert.ok(cps, `expected CPS in orchestrator results; got: ${env.scanners.map(r => r.scanner).join(', ')}`);
});
it('CPS findings carry the token-efficiency category', async () => {
const result = await runScanner('volatile-mid-section/volatile-line-60');
const f = result.findings.find(x => /volatile content inside cached prefix/i.test(x.title || ''));
assert.ok(f);
assert.equal(f.category, 'token-efficiency');
});
});