core/tests/test_suite_membership.py
Claude 68e68589a2
feat(tests): curated-suite membership ratchet — and PR-4's parity pin withdrawn (PR-4, G-7, N-9)
N-9 is the finding, and it PREVENTS work rather than re-scoping it.

WHAT THE PLAN GOT WRONG. N-3 concluded the 23-vs-13 smoke delta was unintended
drift, on two in-repo comments asserting parity, and called that "the strongest
evidence available that the drift was never intended". H-12 repeated it; R-14
built three options on it recommending "raise CI, +46s measured"; PR-4
specified a bidirectional parity pin as deliverable one.

A third artifact answers all of it, and none of them read it.
tests/test_cli_test_suites.py::test_cli_smoke_suite_covers_ci_smoke_gate has
pinned smoke.yml SUBSET-OF TEST_SUITES["smoke"] since before this assessment —
which is why every measurement finds CI-only == 0; a pin, not luck — beneath a
comment headed "DELIBERATELY ONE-DIRECTIONAL — do not 'complete' this by also
asserting local_paths <= ci_paths". Its history is 50fa287d (2026-07-25):
"revert(tests): drop the local-to-CI smoke parity assertion — CI is not a
gate." An earlier agent read the same comment, drew the same inference, made
the assertion symmetric, and reverted it hours later: "That was wrong, and
AGENTS.md says so in a line I had already read."

AGENTS.md:280 — GitHub is "a mirror only; its Actions are billing-locked and
produce dead signals — never chase them." §277 — the workflows are "secondary
observability only." So the local suite is the SOURCE and is deliberately
broader; the delta is the design.

  - PR-4 pin 1: WITHDRAWN. It exists, in the direction that protects something.
  - R-14: premise corrected in the packet. Option A spends 46s on a workflow
    that does not gate; option B would delete real protection for a fiction;
    option C's live half is a two-line comment fix, done here.
  - N-3's exposure claim was too GENEROUS to CI: for a push that skipped the
    local gate there is no automatic check at all. Larger than stated, and not
    closable by editing smoke.yml.

WHAT LANDED. Measured: 749 of 877 test files are in no curated suite. The
plan's mechanism ("assign every orphan, or 749 exclusion reasons") is hollow at
that scale — glob topic-suites ("adr": tests/test_adr_*.py absorbs 172) satisfy
the assertion while changing nothing about what executes, which is precisely the
Third-Door objection the plan levels at G-7's own first formulation. All four
demonstrated incidents (#113, #136, negation-survives-articulation,
speculative-subject-lifecycle) were caused by a NEWLY LANDED orphan; none by a
legacy one.

So: a ratchet. tests/test_suite_membership.py + tests/full_only_baseline.txt.
New orphans fail. The baseline is enforced in BOTH directions — a promoted or
deleted file must leave it, so the list can only shrink — and its count is
pinned so bulk movement is a reviewed decision, in the discipline
test_volume_honesty.py uses.

Also: the gate-guarding pin now runs on the gate. test_cli_test_suites.py lived
in `fast`; the pre-push gate runs smoke + deductive. The pin guarding the gate
did not run on the gate.

Three sabotages, each OBSERVED RED: a new unregistered test file, a stale
baseline entry, the parity pin demoted out of smoke.

Note on the delta: now 12 (local 27, CI 15) because this session added three
pins locally. Under the corrected reading that is the design, not widening
drift — the invariant is CI-only == 0, and that is pinned. The registers now
say so; "ten files" was a measurement, never a target.

Process note: `git checkout --` during sabotage cleanup destroyed uncommitted
edits to core/cli_test.py, which were redone. Backups, not checkout, on files
with unstaged work.

Registers: G-7 CLOSED, H-12 amended, R-14 dissolved in the packet, PR-4
re-specified, N-9 added to plan §0.

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

183 lines
7.3 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``.
"""
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