config-audit/tests/scanners/cache-prefix.test.mjs
Kjell Tore Guttormsen fa1ddd963a feat(cps): scan @imported files for volatile cached-prefix content (v5.10 B6) [skip-docs]
CPS originally inspected only files discovery classifies as claude-md, but a
CLAUDE.md can pull arbitrary files into the cached prefix via @import — and
those targets (e.g. @shared/conventions.md) are usually not claude-md in
discovery, so their inlined content was never scanned. Neither TOK Pattern A
(top-30 of cascade files) nor the in-file CPS scan reaches past the importing
file, so volatility inside an imported file was invisible.

B6 closes the gap: for each @import whose import site sits within the
cached-prefix window (imp.line <= CACHED_PREFIX_LINES), CPS resolves the path
(resolveImportPath, mirroring import-resolver/token-hotspots semantics), reads
the target, and runs findVolatileLines over its first 150 lines. A hit emits a
distinct medium finding — "Volatile content in @imported file breaks cached
prefix" — keyed on the resolved file, evidence naming the importer.

Scope boundaries (deliberate):
- One hop only; imports-of-imports stay with IMP (deep-chain owner).
- No lines-1-30 skip for imported content — that exclusion is root-file-specific
  to avoid Pattern A overlap, which never reaches imported files.
- No double-reporting: an import resolving to a discovered claude-md is skipped
  (own iteration); a reportedImports set dedupes a target imported by several
  CLAUDE.md files.

Dropped from B6 (per plan verdict): confident behavioral cache-buster detection
(opusplan/model-switch is runtime, not static config) and jq-transcript
automation. "No overstated behavioral finding ships" — even the permitted
opusplan info-advisory was left out; the @import extension is the whole of B6.

Byte-stability: the in-file finding keeps the same condition + byte-identical
evidence/description (continue-skip refactored to if-emit, behaviour-preserving);
new findings fire only on a volatile import, which no frozen v5.0.0 fixture has.
docs: README + scanner-internals CPS rows + full B6 note; CLAUDE.md kept lean
([skip-docs]). Suite 1254 -> 1257 green; snapshots + SC-5 untouched.
Version/badges/CHANGELOG wait for the v5.10 release cut.

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

104 lines
5.1 KiB
JavaScript
Raw Permalink 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 — 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');
});
});