config-audit/tests/scanners/optimize-lens-cli.test.mjs
Kjell Tore Guttormsen 1d63492617 fix(acr): optimize lens scopes out plugin-bundled CLAUDE.md + unique candidate paths (M-BUG-11)
The optimize lens CLI fed its precision-gate agent every CLAUDE.md that discovery
returned, including the 256 files under ~/.claude/plugins/ — vendored plugin
CLAUDE.md across every cached version (7 config-audit, 6 ms-ai-architect, 5 okr,
...) plus their bundled tests/fixtures and examples. Running `optimize --global`
on this machine produced 454 candidates across 92 "files", ~250 of them sourced
from plugin-internal files a user cannot act on (the plugin overwrites them on
update). Same class as M-BUG-2: plugin-bundled config is not the user's cascade.

Second defect: candidates were keyed by `relPath || absPath`, but relPath
collides across scopes — a repo-root `CLAUDE.md` and the user-global
`~/.claude/CLAUDE.md` both relPath to `CLAUDE.md`. The two files that actually
matter were merged into one indistinguishable bucket (21 candidates), the agent's
Read(file) would resolve the wrong one, and cache-file relPaths were not readable
relative to cwd at all.

Fix (lens-CLI-local, surgical):
- Filter isPluginBundled (absPath under `.claude/plugins/`) from discovery for
  BOTH halves of the motor (candidate loop + the OPT scanner, which reads
  discovery.files directly). Drops vendored files regardless of active/stale
  version, so excludeCache is unnecessary here.
- Key each candidate by absPath: unique + readable.
No change to file-discovery.mjs or the OPT scanner, so their byte-stable
snapshots are untouched.

Suite 1348/0 (+4: candidate scoping, real-config survives, deterministic scoping,
absolute-path identity). Frozen v5.0.0 + SC-5 snapshots untouched (the lens CLI
has no snapshot; the command is agent-driven, not byte-stable). Dogfood ~/.claude
`optimize --global`: candidates 454->45, deterministic 2->0 (both were stale
plugin-cache copies), distinct files 92->11, repo vs user-global now distinct
(12 + 9 = 21). Residual 45 includes config-audit's own tests/fixtures CLAUDE.md
(repo-specific dogfooding artifact, not a general bug — left alone).
2026-06-30 06:44:36 +02:00

102 lines
4.4 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.

/**
* optimize-lens-cli tests — payload scoping + candidate identity (M-BUG-11).
*
* The lens CLI feeds the precision-gate agent. Two correctness invariants it must
* hold, both established elsewhere in the codebase but originally missing here:
*
* 1. SCOPING — plugin-bundled CLAUDE.md (anything under `.claude/plugins/`:
* vendored plugin config + its bundled tests/fixtures + examples, active or
* stale) is NOT the user's authored config. The user can't act on a
* mechanism-fit suggestion against a file a plugin ships. So the lens must
* drop them — the M-BUG-2 `isPluginBundled` rule, applied to the lens.
*
* 2. IDENTITY — a candidate's `file` must uniquely name a readable file. The
* user-global `~/.claude/CLAUDE.md` and a repo-root `CLAUDE.md` both have
* relPath `CLAUDE.md`; labelling candidates by relPath collides them, and
* the agent's `Read(file)` then resolves the wrong one. Candidates carry an
* absolute path.
*
* Hermetic: a temp target with a real CLAUDE.md and a nested plugin-bundled one.
* `.claude/plugins/` is not in SKIP_DIRS, so the normal walk discovers it —
* no ~/.claude / --global needed.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { join, isAbsolute } from 'node:path';
import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { fileURLToPath } from 'node:url';
const execFileP = promisify(execFile);
const CLI = fileURLToPath(new URL('../../scanners/optimize-lens-cli.mjs', import.meta.url));
/** Build a temp target, run the lens CLI on it, return the parsed payload. */
async function runLens(files) {
const root = await mkdtemp(join(tmpdir(), 'ca-lens-cli-'));
try {
for (const [rel, content] of Object.entries(files)) {
const abs = join(root, rel);
await mkdir(join(abs, '..'), { recursive: true });
await writeFile(abs, content, 'utf-8');
}
const out = join(root, 'payload.json');
await execFileP('node', [CLI, root, '--output-file', out]);
return JSON.parse(await readFile(out, 'utf-8'));
} finally {
await rm(root, { recursive: true, force: true });
}
}
const REAL = '# Project\n\nNever commit secrets to the repo.\n';
const PLUGIN = '# Vendored plugin\n\nNever delete the plugin cache directory.\n';
describe('optimize-lens-cli — scoping (M-BUG-11)', () => {
it('excludes plugin-bundled CLAUDE.md from candidates', async () => {
const payload = await runLens({
'CLAUDE.md': REAL,
'.claude/plugins/cache/mkt/plug/1.0.0/CLAUDE.md': PLUGIN,
});
const bundled = payload.candidates.filter(
(c) => c.file.includes(`.claude/plugins/`) || c.file.includes(`/plugins/`),
);
assert.deepEqual(
bundled.map((c) => c.file),
[],
'no candidate should come from a file under .claude/plugins/',
);
});
it('still surfaces the users real CLAUDE.md', async () => {
const payload = await runLens({
'CLAUDE.md': REAL,
'.claude/plugins/cache/mkt/plug/1.0.0/CLAUDE.md': PLUGIN,
});
const real = payload.candidates.filter((c) => c.file.endsWith(`${join('', 'CLAUDE.md')}`));
assert.ok(real.length >= 1, 'the real CLAUDE.md never-instruction should survive scoping');
});
it('excludes plugin-bundled CLAUDE.md from deterministic findings too', async () => {
const procedure =
'# Plugin release\n\n' +
Array.from({ length: 7 }, (_, i) => `${i + 1}. Do release step ${i + 1} and verify.`).join('\n') +
'\n';
const payload = await runLens({
'CLAUDE.md': REAL,
'.claude/plugins/cache/mkt/plug/1.0.0/CLAUDE.md': procedure,
});
const bundled = (payload.deterministic || []).filter((f) => String(f.file).includes('plugins/'));
assert.deepEqual(bundled, [], 'deterministic CA-OPT-001 must not fire on vendored plugin CLAUDE.md');
});
});
describe('optimize-lens-cli — candidate identity (M-BUG-11)', () => {
it('labels every candidate with an absolute, unique path', async () => {
const payload = await runLens({ 'CLAUDE.md': REAL });
assert.ok(payload.candidates.length >= 1, 'expected at least one candidate');
for (const c of payload.candidates) {
assert.ok(isAbsolute(c.file), `candidate file must be absolute, got: ${c.file}`);
}
});
});