/** * M-BUG-28 — finding IDs identify the CHECK, not the emission position. * * The defect these tests pin (measured 2026-08-09, session #59): `CA-{SCANNER}-{NNN}` * was rendered from a module-global emission counter, so NNN was the finding's * position in that scanner's output for THAT run. Two configurations that both * fail the same check got different IDs for it, and a `.config-audit-ignore` * entry silently retargeted to a neighbouring finding whenever an earlier check * started or stopped firing. * * These assert BEHAVIOUR (run the scanner, read the id), never the mechanism. */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { scan as gapScan } from '../../scanners/feature-gap-scanner.mjs'; import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs'; import { withHermeticHome } from '../helpers/hermetic-home.mjs'; import { startScannerRun } from '../helpers/scanner-run.mjs'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const FIXTURES = resolve(__dirname, '../fixtures'); async function gapIdsByTitle(fixture) { return withHermeticHome(async () => { // Mirror what scan-orchestrator does before each scanner, so this measures // ONLY config-driven ID drift and not counter carry-over between two scans // in one test process. Becomes a no-op once IDs are check-derived. startScannerRun(); const target = resolve(FIXTURES, fixture); const discovery = await discoverConfigFiles(target); const result = await gapScan(target, discovery); const map = new Map(); for (const f of result.findings) map.set(f.title, f.id); return map; }); } describe('finding IDs are stable across configurations (M-BUG-28)', () => { it('gives a check the same ID no matter which other checks fired', async () => { // minimal-project fails more early checks than healthy-project, so under the // emission-counter scheme every shared check downstream of the first // divergence was renumbered. const minimal = await gapIdsByTitle('minimal-project'); const healthy = await gapIdsByTitle('healthy-project'); const shared = [...minimal.keys()].filter((t) => healthy.has(t)); assert.ok(shared.length >= 10, `expected overlapping checks, got ${shared.length}`); const drifted = shared .filter((t) => minimal.get(t) !== healthy.get(t)) .map((t) => `${t}: minimal=${minimal.get(t)} healthy=${healthy.get(t)}`); assert.deepEqual(drifted, [], 'a check must carry one ID regardless of config'); }); it('keeps a suppression pinned to the check the user pinned', async () => { // The user-visible consequence: pin the ID you see, fix an unrelated earlier // gap, and the pin must still name the same check. const minimal = await gapIdsByTitle('minimal-project'); const healthy = await gapIdsByTitle('healthy-project'); const pinned = 'No custom subagents'; assert.ok(minimal.has(pinned) && healthy.has(pinned), 'fixture drifted — check still fires?'); assert.equal(minimal.get(pinned), healthy.get(pinned)); }); });