From 6bdf93185d66bf5e04aba5d43552160a7ec9f34d Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Fri, 31 Jul 2026 13:28:18 -0700 Subject: [PATCH] fix(tests): derive the retired-invocation guard's file list from git The guard walked the filesystem with rglob, so on the main checkout it read gitignored working material: tmp/context/** holds `git archive` exports of pre-retirement revisions, every one of which names the retired tools legitimately. Fourteen such lines made `pytest` fail on main while passing in every worktree. That asymmetry is the part worth recording. A worktree structurally lacks both of the directories that break this guard - it has no tmp/, and the nested .claude/worktrees/ only exists under the main checkout - and every gate run for C6, D5 and D6a was made inside a worktree. The guard could not see the thing that breaks it from the place we always ran it. Derive the list from git instead: tracked plus untracked-not-ignored. Untracked-not-ignored is included deliberately, because a newly written file is exactly the case this guard exists for and it is usually not staged yet. Gitignored files become invisible, which is the intent - they are not the codebase - and that limit is now stated in the docstring alongside the other one it already cannot see, invocation strings composed at runtime. The precedent is in the repo: the caller census added in D6a derives from git for these same two reasons, and its docstring names both. This guard predates it and had not learned the lesson. Teeth verified rather than assumed: a new untracked-not-ignored file naming a retired invocation turns it red, removing it turns it green, and a gitignored file leaves it green as documented. Gates on the main checkout: 2705 passed / 1 skipped (was 1 failed), ruff clean, phase0 anchor ic 0.9600 / annual 0.8408, 32/32 configs, frozen baseline 77/77. --- tests/test_hand_anchor_record.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/test_hand_anchor_record.py b/tests/test_hand_anchor_record.py index f9aa488..fef8aff 100644 --- a/tests/test_hand_anchor_record.py +++ b/tests/test_hand_anchor_record.py @@ -15,6 +15,7 @@ from __future__ import annotations import json +import subprocess from pathlib import Path import pytest @@ -287,14 +288,35 @@ def test_no_new_file_starts_instructing_people_to_run_a_retired_tool(): is exactly the #82 shape — a guard that only looks where you already looked confirms only what you already know. `docs/progress/` is excluded as a historical archive: it records what was true then. + + SCOPE OF THE FILE LIST. Derived from git (tracked ∪ untracked-not-ignored), + never from a directory walk. A walk sweeps in gitignored working material — + `tmp/context/**` holds `git archive` exports of PRE-RETIREMENT revisions, so + every one of them names the retired tools legitimately — and inside a + worktree it follows the `artifacts` symlink into a different checkout. Both + were observed: this guard was red on the main checkout while green in every + worktree, because a worktree structurally lacks the directories that break + it. Untracked-but-not-ignored IS included on purpose: a newly written file + is exactly the case this guard exists for, and it is usually not staged yet. + + WHAT IT STILL CANNOT SEE: gitignored files (deliberate — they are not the + codebase), and invocation strings composed at runtime. """ repo = Path(__file__).resolve().parents[1] + + def _git(*args: str) -> list[str]: + out = subprocess.run( + ["git", "-C", str(repo), *args], capture_output=True, text=True, check=True + ).stdout + return [line for line in out.splitlines() if line] + + listed = set(_git("ls-files")) | set(_git("ls-files", "--others", "--exclude-standard")) offenders: list[str] = [] - for path in sorted(repo.rglob("*")): + for relative in sorted(listed): + path = repo / relative if path.suffix not in {".py", ".md", ".yaml", ".txt"} or not path.is_file(): continue - relative = path.relative_to(repo).as_posix() - if relative.startswith(("docs/progress/", ".git/", "tests/")): + if relative.startswith(("docs/progress/", "tests/")): continue text = path.read_text(encoding="utf-8") for call in RETIRED_INVOCATIONS: