diff --git a/scanners/lib/file-discovery.mjs b/scanners/lib/file-discovery.mjs index 7f32bad..eb6e832 100644 --- a/scanners/lib/file-discovery.mjs +++ b/scanners/lib/file-discovery.mjs @@ -11,6 +11,11 @@ const SKIP_DIRS = new Set([ 'node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__', '.next', '.nuxt', '.output', '.cache', '.turbo', '.parcel-cache', 'vendor', 'venv', '.venv', '.tox', + // A `backups` dir holds backup COPIES, not live config — auditing it as if + // live produces stale findings. config-audit's own session backups + // (~/.claude/config-audit/backups//files/.../CLAUDE.md) are the canonical + // case (M-BUG-8), but the rule is general: backups are never live config. + 'backups', ]); // Path marker for the plugin install cache (~/.claude/plugins/cache). diff --git a/tests/lib/file-discovery.test.mjs b/tests/lib/file-discovery.test.mjs index 036fe4f..3a4a09f 100644 --- a/tests/lib/file-discovery.test.mjs +++ b/tests/lib/file-discovery.test.mjs @@ -1,6 +1,6 @@ import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; -import { join } from 'node:path'; +import { join, sep } from 'node:path'; import { mkdir, writeFile, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { @@ -509,3 +509,49 @@ describe('discoverConfigFiles — cache-aware filtering', () => { assert.ok(!keys.has(STALE1) && !keys.has(STALE2), 'stale dropped via Multi'); }); }); + +// ─────────────────────────────────────────────────────────────── +// Group 8: backups/ skip (M-BUG-8) — a directory named `backups` +// holds backup COPIES, not live config. config-audit's own session +// backups (~/.claude/config-audit/backups//files/plugins/*/CLAUDE.md) +// were walked as if live, producing stale findings on a ~/.claude audit. +// ─────────────────────────────────────────────────────────────── + +describe('discoverConfigFiles — skips backups/ (M-BUG-8)', () => { + let dir; + + before(async () => { + dir = tempDir('backups'); + // A live CLAUDE.md at the root (must still be discovered). + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, 'CLAUDE.md'), '# Live config'); + // config-audit's own backup tree — frozen copies, NOT live config. + const backupClaude = join(dir, 'config-audit', 'backups', '20260518_103233', 'files', 'plugins', 'voyage'); + await mkdir(backupClaude, { recursive: true }); + await writeFile(join(backupClaude, 'CLAUDE.md'), '# Frozen backup copy'); + const backupSettings = join(dir, 'config-audit', 'backups', '20260518_103233', 'files', '.claude'); + await mkdir(backupSettings, { recursive: true }); + await writeFile(join(backupSettings, 'settings.json'), '{}'); + }); + + after(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it('discovers the live CLAUDE.md', async () => { + const { files } = await discoverConfigFiles(dir); + const live = files.find(f => f.relPath === 'CLAUDE.md'); + assert.ok(live, 'live CLAUDE.md at root should be discovered'); + }); + + it('does NOT discover config files under a backups/ directory', async () => { + const { files } = await discoverConfigFiles(dir); + const inBackups = files.filter(f => f.absPath.includes(`backups${sep}`)); + assert.equal(inBackups.length, 0, 'no files under backups/ should be discovered'); + }); + + it('counts backups/ as a skipped directory', async () => { + const { skipped } = await discoverConfigFiles(dir); + assert.ok(skipped >= 1, 'backups/ should be counted as skipped'); + }); +});