test(tooling): T-1257 — reach called by name, and one Python

Two failures T-1261 found and fixed by hand, left unguarded. They now fail
the conformance gate:

- (8) An interpreter-prefixed invocation — `python -m tooling`,
  `python3 -m tooling`, `.venv/bin/reach` — anywhere in the Makefile, the
  hooks, .claude or docs. One such line silently forks the PATH guarantee.
  Lines that FORBID the pattern are exempt, since docs/DEVOPS.md states the
  rule in those words and a check that flags its own documentation gets muted.
- (9) The installed reach (read from its shebang) and the .venv the gate
  tests run in must be the same Python minor version, and it must be the one
  the Makefile pins. uv once installed reach on 3.11 against a 3.14 venv, so
  the tests were proving nothing about the interpreter the hooks run.

Both proven to fail: an appended `python3 -m tooling.main` make target is
caught at its line, and a fake `reach` with a 3.12 shebang first on PATH
trips both version comparisons. The invariant list in the docstring now
covers all nine, not the first five.

Also: the canvas-version units ran under system python3 while every other
gate test uses $(VENV_PY). Now they match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 20:21:32 +02:00
co-authored by Claude Opus 5.5
parent 876cb8256e
commit caea76387b
2 changed files with 87 additions and 2 deletions
+1 -1
View File
@@ -304,7 +304,7 @@ test-tooling:
{ echo " FAIL: character decisions — log follows:"; cat .cache/test-tooling-character.log; exit 1; }
@echo " [test-tooling] canvas-generation version gate units (T-1242)..."
@mkdir -p .cache
@python3 tooling/test_canvas_version_check.py 2> .cache/test-tooling-canvas-version.log || \
@$(VENV_PY) tooling/test_canvas_version_check.py 2> .cache/test-tooling-canvas-version.log || \
{ echo " FAIL: canvas version gate units — log follows:"; cat .cache/test-tooling-canvas-version.log; exit 1; }
@echo " [test-tooling] reach lazy domain registration (T-1260)..."
@mkdir -p .cache
+86 -1
View File
@@ -23,6 +23,12 @@ Invariants:
5. Every ReachError names a remedy — a `fix=` on every raise site. The hardest
to enforce and the most valuable: an error that only says "no" is the thing
D-263 exists to replace.
6. Discovery succeeds and being wrong fails (exit codes).
7. The Blender carve-out stays outside the package, and stays populated.
8. reach is invoked by its bare name — never `python -m tooling` or a venv
path — in the Makefile, hooks, .claude and docs (T-1257, from T-1261).
9. The installed reach and the .venv the gate tests run in are the same
Python minor version, and it is the one the Makefile pins (T-1257).
The import-graph invariant (nothing heavy reachable from main.py) lives in
test_lazy_domains.py rather than being duplicated here.
@@ -359,6 +365,82 @@ def check_carve_out_stays_carved(failures: list[str]) -> None:
)
def check_invoked_through_path(failures: list[str]) -> None:
"""(8) reach is invoked by NAME, never through an interpreter or a venv path.
The PATH guarantee (T-1261) is that `reach` means the same program in a
shell, a hook and an agent. One `python -m tooling …` or `.venv/bin/reach`
in a make target or a hook silently forks that — it runs whichever
interpreter happens to be first, with whichever dependencies it has — and
nothing fails until a version-specific bug shows in one path and not the
other. T-1261 verified this by hand; this makes it permanent (T-1257).
"""
patterns = ("python -m tooling", "python3 -m tooling", ".venv/bin/reach")
roots = [REPO_ROOT / "Makefile", REPO_ROOT / ".config" / "hooks", REPO_ROOT / ".claude", REPO_ROOT / "docs"]
for root in roots:
files = [root] if root.is_file() else [p for p in root.rglob("*") if p.is_file()]
for path in files:
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
continue
for lineno, line in enumerate(text.splitlines(), 1):
# A line that FORBIDS the pattern is the rule, not a breach of
# it (docs/DEVOPS.md: "Never `python -m tooling`"). A check that
# flags its own documentation gets muted.
if "never" in line.lower():
continue
for pattern in patterns:
if pattern in line:
failures.append(
f"[path] {path.relative_to(REPO_ROOT)}:{lineno} invokes reach through "
f"'{pattern}' — call the bare name `reach` (T-1261's PATH guarantee)"
)
def check_one_python(failures: list[str]) -> None:
"""(9) The installed reach and the .venv run the same Python minor version.
T-1261 found uv had installed the reach tool on CPython 3.11 while .venv
and system python were 3.14 — uv picks the LOWEST interpreter satisfying
requires-python. The Makefile now pins PYTHON_VERSION for both, but nothing
noticed a divergence: the gate tests run under .venv, the hooks run reach,
and a bug in one interpreter would pass the tests that ran on the other.
"""
import re
import shutil
reach = shutil.which("reach")
if reach is None:
return # check_exit_codes already reports a missing reach
shebang = Path(reach).read_text(encoding="utf-8", errors="replace").splitlines()[0]
if not shebang.startswith("#!"):
failures.append(f"[python] {reach} has no shebang — cannot tell which Python runs reach")
return
interpreter = shebang[2:].strip().split()[0]
probe = subprocess.run(
[interpreter, "-c", "import sys; print('%d.%d' % sys.version_info[:2])"],
capture_output=True, text=True,
)
tool_version = probe.stdout.strip()
venv_version = "%d.%d" % sys.version_info[:2]
makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")
pinned = re.search(r"^PYTHON_VERSION \?= (\S+)", makefile, re.M)
pinned_version = pinned.group(1) if pinned else None
if tool_version != venv_version:
failures.append(
f"[python] reach runs on Python {tool_version} ({interpreter}) but the gate tests run on "
f"{venv_version} — tests prove nothing about the interpreter the hooks use. "
"Fix: make install-reach && make setup-venv (both pin PYTHON_VERSION)"
)
if pinned_version and tool_version != pinned_version:
failures.append(
f"[python] reach runs on Python {tool_version}, the Makefile pins {pinned_version}. "
"Fix: make install-reach"
)
def main() -> int:
if shutil.which("reach") is None:
print("test_conformance: `reach` is not on PATH.\n Fix: make install-reach", file=sys.stderr)
@@ -373,6 +455,8 @@ def main() -> int:
check_errors_name_a_remedy(failures)
check_exit_codes(failures)
check_carve_out_stays_carved(failures)
check_invoked_through_path(failures)
check_one_python(failures)
if failures:
print("test_conformance: FAIL", file=sys.stderr)
@@ -381,7 +465,8 @@ def main() -> int:
return 1
print("test_conformance: OK — transport isolated, one output path, "
"every command decorated and helped, every error names a remedy")
"every command decorated and helped, every error names a remedy, "
"reach called by name, one Python")
return 0