/** * Fix verification must identify a finding INSTANCE, not just its check * (M-BUG-28, prediction 7). * * A finding ID names the check, so one check failing in two files yields two * findings sharing an ID. If verification keys on the ID alone, fixing one of * them marks both as fixed — and the untouched one, still present in the * re-scan, is then reported as a REGRESSION. The user is told a fix came back * undone when it was never applied. * * `(findingId, file)` is the instance key, and both `planFixes` and `applyFixes` * already carry `file` next to `findingId`. */ import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { runAllScanners } from '../../scanners/scan-orchestrator.mjs'; import { planFixes, applyFixes, verifyFixes } from '../../scanners/fix-engine.mjs'; const RULE = (name) => `--- globs: - "src/**/*.ts" --- # ${name} A path-scoped rule that uses the undocumented \`globs:\` key. `; describe('fix verification distinguishes two instances of one check', () => { let dir; before(async () => { dir = await mkdtemp(join(tmpdir(), 'ca-mbug28-')); await mkdir(join(dir, '.claude', 'rules'), { recursive: true }); await writeFile(join(dir, 'CLAUDE.md'), '# Project\n\nInstructions.\n'); await writeFile(join(dir, '.claude', 'rules', 'alpha.md'), RULE('Alpha')); await writeFile(join(dir, '.claude', 'rules', 'beta.md'), RULE('Beta')); }); after(async () => { if (dir) await rm(dir, { recursive: true, force: true }); }); it('does not report the untouched instance as a regression', async () => { const envelope = await runAllScanners(dir, { filterFixtures: false, suppress: false }); const globsFindings = envelope.scanners .flatMap((s) => s.findings) .filter((f) => f.title === 'Rule uses "globs" instead of documented "paths"'); assert.equal(globsFindings.length, 2, 'fixture must produce one finding per rule file'); assert.equal( globsFindings[0].id, globsFindings[1].id, 'both instances name the same check — that is the scheme, not a defect' ); const { fixes } = planFixes(envelope); const alphaFix = fixes.filter((p) => p.file && p.file.endsWith('alpha.md')); assert.equal(alphaFix.length, 1, 'expected exactly one fix plan for alpha.md'); const backupDir = join(dir, '.backups'); await mkdir(backupDir, { recursive: true }); const { applied } = await applyFixes(alphaFix, { backupDir }); assert.equal(applied.length, 1); const { verified, regressions } = await verifyFixes(envelope, applied); // beta.md was never fixed. It is still open — which is not a regression. assert.deepEqual(regressions, [], 'an unfixed sibling instance must not count as a regression'); assert.equal(verified.length, 1, 'exactly the fixed instance is verified'); }); });