"""Guard against a renamed stage leaving its old path standing in source. `wiki/` was renamed to `kb/` on 2026-08-21. The directory moved; the string did not, in 33 places - error messages, `--help` text, docstrings and the lint report's own header, all naming a directory that no longer exists. Nothing caught it, because no check reads a path literal in source. This is that check. It is deliberately a plain substring scan over the source tree rather than a `docs verify` check: `docs verify` reads `shipped_prose()`, which is markdown only, and the bulk of the defect sat in `.py` strings. """ from __future__ import annotations from pathlib import Path TOOLS_DIR = Path(__file__).resolve().parents[2] # Retired stage path -> what replaced it. A future rename adds a row here in the # same change that does the renaming, which is what makes the next occurrence a # test failure instead of a string nobody reads for a year. RETIRED_STAGE_PATHS = { "wiki/": "kb/", } # Occurrences that are not stage paths at all. Kept as an explicit list with a # reason rather than dodged by a cleverer regex: an exception a reader can see # is worth more than one a pattern hides. ALLOWED = { # A fixture release URL, where `wiki` is a repository name in `owner/repo`. ("tests/test_dist_cmd.py", "https://example/torben/wiki/releases/tag/v0.3.1"), } SELF = Path(__file__).resolve() def _scanned_files() -> list[Path]: """Every source file in `tools/` a stale path literal could hide in. This module is excluded, and has to be: it is the one file whose job is to name the retired paths, so scanning it would make the guard fail on its own declaration. """ candidates = [*(TOOLS_DIR / "chemenu").rglob("*.py"), TOOLS_DIR / "wikitool"] return sorted(path for path in candidates if path.resolve() != SELF) def test_no_retired_stage_path_survives_in_source(): findings = [] for path in _scanned_files(): relative = path.relative_to(TOOLS_DIR / "chemenu" if path.suffix == ".py" else TOOLS_DIR) for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): for retired, replacement in RETIRED_STAGE_PATHS.items(): if retired not in line: continue if any(key == str(relative) and excerpt in line for key, excerpt in ALLOWED): continue findings.append( f"{relative}:{number} names the retired path `{retired}` " f"(now `{replacement}`): {line.strip()}" ) assert findings == [], "Retired stage paths still in source:\n" + "\n".join(findings) def test_the_guard_actually_scans_something(): """A scan that silently matches no file passes for the wrong reason.""" scanned = _scanned_files() assert len(scanned) > 40 assert TOOLS_DIR / "wikitool" in scanned assert SELF not in scanned