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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user