core/tests/test_suite_membership.py
Claude edf6c2a454
fix(tests): the membership ratchet caught its author — baseline 749 -> 748
PR-6 promoted tests/test_safety_pack.py onto the gate and left it listed in
tests/full_only_baseline.txt. test_baseline_has_no_stale_entries failed the very
next smoke run, naming the file and the direction.

That is the both-directions half of the ratchet doing exactly the work it was
written for, roughly an hour after it landed, against the person who wrote it.
A baseline that silently keeps entries it no longer needs stops being a
measurement and becomes a story about one — so the failure is the feature.

Recorded in the pin's own docstring and in G-7, because a mechanism catching its
author is better evidence than a sabotage test: the sabotage was contrived, this
was not.

Baseline: 749 -> 748. The ratchet only turns down.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wcw2pnMBwyvmNyQg4uPEt4
2026-07-28 04:22:28 +00:00

189 lines
7.7 KiB
Python

"""PR-4 / G-7 — a test file may not land outside every curated suite.
The failure mode, demonstrated four times in this repository: a pin is written
for a real defect, lands in no curated suite, therefore runs on no pre-merge
gate, and the thing it pins regresses anyway. #113 (``test_adr_index``), #136,
``test_negation_survives_articulation``, and — while fixing H-13 on 2026-07-28
— ``test_speculative_subject_lifecycle``, which pinned the exact behavior that
had silently broken. Each was repaired individually, after shipping.
**Why this is a ratchet and not a membership assertion.** The plan originally
specified "every ``tests/**/test_*.py`` belongs to >=1 suite excluding ``full``,
or to a registered exclusion list with a reason." Measured at 2026-07-28 that is
749 files against 128 curated. Both ways of satisfying it at that scale are
hollow: glob-defined topic suites (``"adr": ("tests/test_adr_*.py",)`` alone
absorbs 172) make the assertion pass while changing nothing about what executes,
and 749 hand-written exclusion reasons are ceremony. A curated suite nobody runs
is the same non-guarantee as ``full``, wearing a different name — which is the
objection the plan itself raises against G-7's first formulation.
None of the 749 legacy files caused any of the four incidents. A *new* orphan
caused all four. So the pin with teeth is the one that blocks new orphans and
makes the legacy set a declared, monotonically shrinking number.
Recorded as N-9 in ``docs/assessment/50-execution-plan.md``.
It caught its author within the hour: PR-6 promoted ``tests/test_safety_pack.py``
onto the gate and left it listed here, and ``test_baseline_has_no_stale_entries``
failed the smoke run. That is the both-directions half doing the work it was
written for — a baseline that silently keeps entries it no longer needs stops
being a measurement and becomes a story about one.
"""
from __future__ import annotations
import ast
import glob
import re
from pathlib import Path
import pytest
_ROOT = Path(__file__).resolve().parents[1]
_BASELINE = _ROOT / "tests" / "full_only_baseline.txt"
def _curated_files() -> set[str]:
"""Every test path reachable from a curated suite other than ``full``.
``TEST_SUITES`` is parsed from source rather than imported so this pin keeps
working if importing the CLI ever acquires a side effect; the tuple is a
literal by construction (``test_cli_test_suites.py`` pins its shape).
"""
src = (_ROOT / "core" / "cli_test.py").read_text(encoding="utf-8")
match = re.search(r"TEST_SUITES\s*(?::[^=]+)?=\s*\{", src)
assert match is not None, "TEST_SUITES literal not found in core/cli_test.py"
start = match.end() - 1
depth = 0
end = len(src)
for i, ch in enumerate(src[start:], start):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
end = i + 1
break
suites = ast.literal_eval(src[start:end])
curated: set[str] = set()
for name, patterns in suites.items():
if name == "full":
# ``full`` is ("tests/",) — a DIRECTORY, so it matches every test
# file trivially. Counting it would make this pin vacuous; that
# scan artifact is N-1 in the execution plan and has now produced
# three false conclusions in this repository.
continue
for pattern in patterns:
curated.update(str(Path(p).as_posix()) for p in glob.glob(pattern))
return curated
def _all_test_files() -> set[str]:
return {
str(Path(p).as_posix())
for p in glob.glob("tests/**/test_*.py", recursive=True)
}
def _baseline() -> list[str]:
lines = _BASELINE.read_text(encoding="utf-8").splitlines()
return [ln.strip() for ln in lines if ln.strip() and not ln.lstrip().startswith("#")]
def test_baseline_file_exists_and_is_sorted_and_unique() -> None:
entries = _baseline()
assert entries, "the baseline is empty — it should list the known full-only tests"
assert entries == sorted(entries), "baseline must stay sorted (stable diffs)"
assert len(entries) == len(set(entries)), "baseline contains duplicates"
def test_no_new_full_only_test_files() -> None:
"""A new test file must join a curated suite, or the gate it needs is absent.
If this fails on a file you just added: put it in the suite whose lane it
actually belongs to in ``core/cli_test.py::TEST_SUITES``. Adding it to the
baseline instead is available and is almost always the wrong answer — the
baseline is for files that predate this pin, not an opt-out.
"""
orphans = _all_test_files() - _curated_files()
unlisted = sorted(orphans - set(_baseline()))
assert not unlisted, (
"these test files are in no curated suite and are not in the recorded "
"baseline, so they run on no pre-merge gate:\n "
+ "\n ".join(unlisted)
+ "\n\nRegister each in core/cli_test.py::TEST_SUITES."
)
def test_baseline_has_no_stale_entries() -> None:
"""The list may only shrink — a promoted or deleted file must leave it.
Enforced in BOTH directions, in the discipline ``test_volume_honesty.py``
uses: a baseline that silently keeps entries it no longer needs stops being
a measurement and becomes a story about one.
"""
curated = _curated_files()
present = _all_test_files()
entries = _baseline()
vanished = sorted(e for e in entries if e not in present)
assert not vanished, (
"baseline names files that no longer exist — delete these lines:\n "
+ "\n ".join(vanished)
)
promoted = sorted(e for e in entries if e in curated)
assert not promoted, (
"these files are now in a curated suite (good) but are still listed as "
"full-only — delete their baseline lines so the count reflects reality:\n "
+ "\n ".join(promoted)
)
def test_recorded_debt_matches_the_measurement() -> None:
"""Pin the number so a bulk change to it shows up as a reviewed decision.
749 at 2026-07-28. Lowering this is the point; raising it must be argued
for, not slipped in. The assertion names the direction so a reviewer of a
failing run knows immediately which one happened.
"""
entries = _baseline()
assert len(entries) <= 749, (
f"full-only test debt grew to {len(entries)} (was 749). The ratchet only "
"turns one way — register the new files in a curated suite instead."
)
def test_the_gate_guarding_pin_runs_on_the_gate() -> None:
"""The parity pin must itself be in ``smoke`` (N-9).
``test_cli_smoke_suite_covers_ci_smoke_gate`` enforces that the local suite
is never narrower than ``smoke.yml``. It lived in ``fast``, while the
pre-push gate runs ``smoke`` + ``deductive`` — so the pin guarding the gate
did not run on the gate. This keeps it there.
"""
curated_src = (_ROOT / "core" / "cli_test.py").read_text(encoding="utf-8")
match = re.search(r"TEST_SUITES\s*(?::[^=]+)?=\s*\{", curated_src)
assert match is not None
start = match.end() - 1
depth = 0
end = len(curated_src)
for i, ch in enumerate(curated_src[start:], start):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
end = i + 1
break
suites = ast.literal_eval(curated_src[start:end])
assert "tests/test_cli_test_suites.py" in suites["smoke"], (
"the smoke/CI parity pin must run on the gate it protects"
)
def test_this_pin_is_itself_registered() -> None:
"""No exemption for the membership pin — #136's lesson applied to itself."""
orphans = _all_test_files() - _curated_files()
assert "tests/test_suite_membership.py" not in orphans