From bd31cf2bba541c0b2b0ce45e9be0f845dd690a97 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 12 Aug 2026 20:08:35 -0400 Subject: [PATCH] fix(#3321): exclude .claude/.planning from no-phantom-issue-refs SKIP_DIRS (#3396) * test(#3321): add failing-first regression proving walk() must skip .claude/.planning RED step: SKIP_DIRS does not yet exclude .claude/.planning, so this test is expected to fail until the next commit adds them. * fix(#3321): exclude .claude/.planning from no-phantom-issue-refs SKIP_DIRS walk() previously had no guard against descending into ambient, gitignored .claude/worktrees/** or .planning/** content, so this guard's pass/fail would have depended on the developer's local worktree layout the next time PHANTOM is repopulated. Currently dormant (PHANTOM=[] short-circuits via t.skip before walk() runs) but the gap was real, per #1885 F23. --------- Co-authored-by: sim --- tests/no-phantom-issue-refs.test.cjs | 41 +++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/no-phantom-issue-refs.test.cjs b/tests/no-phantom-issue-refs.test.cjs index fbf78260a..a4233b6f4 100644 --- a/tests/no-phantom-issue-refs.test.cjs +++ b/tests/no-phantom-issue-refs.test.cjs @@ -53,7 +53,7 @@ function buildRefRe(phantom) { const REF_RE = buildRefRe(PHANTOM); const SCAN_EXT = new Set(['.md', '.cjs', '.js', '.cts', '.ts']); -const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.changeset']); +const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.changeset', '.claude', '.planning']); // This guard file itself names the phantom numbers (by necessity); exclude it. const SELF = path.relative(ROOT, __filename); @@ -131,6 +131,45 @@ test('walk() skips broken symlinks and does not throw ENOENT (#1545)', (t) => { } }); +test('walk() does not scan ambient .claude/.planning content (#3321)', () => { + const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'nophantom-ambient-')); + try { + fs.writeFileSync(path.join(fixture, 'real.md'), '# real, no phantom refs\n'); + + const claudeDir = path.join(fixture, '.claude', 'worktrees', 'some-agent-worktree'); + fs.mkdirSync(claudeDir, { recursive: true }); + fs.writeFileSync(path.join(claudeDir, 'offender.md'), '#94729 sentinel phantom ref\n'); + + const planningDir = path.join(fixture, '.planning', 'notes'); + fs.mkdirSync(planningDir, { recursive: true }); + fs.writeFileSync(path.join(planningDir, 'offender.md'), '#94729 sentinel phantom ref\n'); + + const found = walk(fixture, []).map((f) => path.relative(fixture, f)); + + assert.ok(found.includes('real.md'), 'walk() must still include real.md at fixture root'); + assert.ok( + !found.some((f) => f.startsWith('.claude' + path.sep)), + 'walk() must NOT descend into .claude/', + ); + assert.ok( + !found.some((f) => f.startsWith('.planning' + path.sep)), + 'walk() must NOT descend into .planning/', + ); + + // Prove the offending content WOULD have matched if scanned -- i.e. this isn't a vacuous + // "no file happened to match" pass. Use a test-local sentinel, never the module PHANTOM. + const sentinelRe = buildRefRe(['94729']); + const offenderContent = fs.readFileSync(path.join(claudeDir, 'offender.md'), 'utf8'); + assert.ok( + sentinelRe.test(offenderContent), + 'sanity: sentinel ref pattern must actually match the offender content', + ); + } finally { + // eslint-disable-next-line local/no-raw-rmsync-in-tests -- local cleanup in standalone guard test; no helpers import available (would introduce a test-dep cycle) + fs.rmSync(fixture, { recursive: true, force: true }); + } +}); + test('buildRefRe() boundaries — empty list is inert, single/double entries match exactly', () => { assert.strictEqual(buildRefRe([]), null, 'empty list must build no regex');