test: the pytest half of the skip-loop sweep that #998 did not ship (#994) - #1000
Conversation
…994) #998 added `test/selftest/470` and no pytest twin. CONTEXT.md is explicit that a test living in one harness is not finished, and neither the author nor the reviewer caught it. This is that half. IT IS NOT A PORT, and the difference is why it is worth writing rather than an excuse for having written it. test/selftest/470 measures the CORPUS a loop that drifted from its arms test_skip_loop_arms.py measures the INSTRUMENT a classifier that stopped being able to tell A classifier that filed every site as `armless` would report zero mismatches, and 470's population premises would still pass on whatever loops remained. So this drives the same tool over PLANTED trees whose right answer is known, and asserts the classification itself. Six tests, seventeen checks. The clean site is compared rather than quietly filed as armless or interpolated; a renamed sibling is caught; the clean and drifted fixtures must DISAGREE, or neither of those arms is evidence; an armless branch and an interpolated one are each counted as themselves; and `compared + armless + interpolated == loops`, so no site falls out of the report. REMOVAL PROOFS, each mutation asserted to have applied before the run, because a clean pass reads identically whether the code is load-bearing or the edit never landed: stop reporting mismatches -> the two rename arms go red compare the interpolated site -> the interpolated and partition arms go red The second is the one worth keeping: removing the tool's REFUSAL to compare manufactures a false mismatch on a correct site, and a guard that invents a red is the guard people switch off. It drives .github/scripts/skip-loop-arms.py by subprocess and never the shell harness. That is a python tool rather than test/lib.sh, so this is a second measurement and not the first one wearing a python wrapper. Two guards in the tree caught my omissions while writing it, which is the argument for both of them: `test_harness_deps` named the file as undeclared the moment it appeared, and `test_docs_cover_the_corpus` refused it until TESTS.md named every test. Neither was told; both derived it from the corpus. Verified: 255 passed / 580 checks / 0 fail over the 14 files the CI job runs, in a venv pinned to requirements-test.txt with psycopg absent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbyGSaU93XYQr8aH4NrUiw
…ke (#994) Review round on #1000, from @OffgridwithJD. THE CONTROL WAS THIN AND THE MEASUREMENT SAYS SO. It compared two trees -- clean reports nothing, drifted reports something -- which restates the two arms above it with an `and`. A classifier that reports a mismatch for the WRONG REASON passes it. Driven, rather than argued. Mutant: report a mismatch but name the wrong line. new control got ["two.sh:1"] want ["two.sh:12"] FAIL old control clean=0 drift=1, asserts 0/1 PASSES, blind So the control now plants BOTH loops in one file, the clean one above and the drifted one below, asserts as a premise that both were compared, and pins the reported site to the drifted loop's own line. That is the only form that can tell "it found my bug" from "it found something". AND THE PARTITION ARM IS NOT THE GUARD, which is now written where a reader will find it instead of being left to work out. `compared + armless + interpolated == loops` is the strongest line in the file and the cheapest to satisfy wrongly: a classifier that filed everything as `armless` satisfies it perfectly. It is load-bearing only because the per-bucket arms assert that a KNOWN site lands in the RIGHT bucket. Both halves or neither. The rename was caught in BOTH directions by test_docs_cover_the_corpus -- a documented test that no longer exists, and an existing test not documented -- which is the third time tonight a guard in this tree has decided something rather than been told it. Verified: 255 passed / 581 checks / 0 fail over the 14 files the CI job runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbyGSaU93XYQr8aH4NrUiw
|
Reviewed from the file list first, which is the lesson from the bundles and not a formality. Four text files, nothing derived, nothing binary, nothing outside Your two questions, answered — and on the first one your own removal proof settles it better than my opinion does. 1. "Planted trees" is a second measurement, not a rationalisationCompare the interpolated site → the interpolated and partition arms go red. That mutation is invisible to 2. Your new control does real work, and I checked the thing that would have made it circular
drift_line = next(i + 1 for i, l in enumerate(body.splitlines())
if l.lstrip().startswith("for _b in"))— not from the tool's output. That is the difference between a known answer and an identity. Had it been parsed out of the And The per-bucket negatives are what make the partition load-bearing, and you put that in words: a classifier filing everything as One finding, non-blocking: the pytest half cannot see an unusable toolNo test asserts the tool's exit code. I mutated
The sharpest detail is which arm caught it in the shell half. Not the headline — My first attempt at this probe was invalid and the premise is what said so. I appended Two things I would not change
And the docstring that distinguishes the two halves — corpus versus instrument — belongs in the file rather than in a PR description that nobody reads again. On the debt itself
Verified here: 🤖 Generated with Claude Code |
Review round two on #1000, from @OffgridwithJD. EVERY TEST HERE READS THE TOOL'S STDOUT AND NONE NOTICED IT BECOMING UNUSABLE. Reproduced before fixing, with the behaviour asserted to have moved rather than only the edit to have applied: mutation: return 2 from main(), counters still printed correctly tool rc 0 -> 2 behaviour moved pytest half 6 passed / 18 checks BLIND after this commit 6 failed caught The shell half already catches it, through `|| _sk_out="TOOL FAILED"`, so the pair was stronger than this half alone. THE SHARPEST PART IS WHICH ARM CAUGHT IT THERE: the headline `every comparable skip loop names exactly the arms its sibling would emit` stayed GREEN under the mutation, because with no parseable output there are no mismatches to report. A premise failed. An empty finding list and a tool that cannot run are the same observation to an arm that only counts findings. So the assertion goes in `_sweep`, once, covering all six tests -- for that reason rather than for tidiness. The partition test built its own subprocess call and had the same blindness, so it is routed through the helper too. AND THE REPORTER'S OWN FIRST PROBE WAS INVALID, which is worth recording beside the fix: `sys.exit(2)` appended at module level, after the `__main__` guard had already exited. The edit applied cleanly and the behaviour did not move -- rc still 0. An arm asserting only that a mutation APPLIED would have let that be reported as a blindness on no evidence. `a-mutation-that-did-not-apply` needs its second half: assert the behaviour moved, not the text. Verified: 255 passed / 587 checks / 0 fail over the 14 files the CI job runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbyGSaU93XYQr8aH4NrUiw
OffgridwithJD
left a comment
There was a problem hiding this comment.
Approving at ac989b4d on 13 of 13 green. The finding is fixed and I verified the fix the three-step way rather than reading the diff.
unmutated 6 passed, 24 checks (18 before: the assertion adds one per test)
1. the edit landed the last `return 0` -> `return 2`
2. the behaviour moved tool rc 0 -> 2, counters still printed correctly
3. which checks moved 6 FAILED, including the partition test
All six, not one — which is what putting it in _sweep buys, and routing the partition test through the helper closed the one call site that had its own blindness.
The docstring is the part I would keep. It records that the shell half caught this through || _sk_out="TOOL FAILED" while a premise failed and the headline arm stayed green, and it says why that argues for the helper rather than for one test. Your generalisation of it is sharper than my finding was: an empty finding list and a tool that cannot run are the same observation to any arm that only counts findings. That is true of most headline arms in this harness, and the premise beside each one is the load-bearing half.
It also records that my first probe of this was invalid — sys.exit(2) appended after the __main__ guard, which applied cleanly and changed nothing. Asserting a mutation applied is not asserting the behaviour moved, and a file that says so where the next person will read it is worth more than the arm it explains.
On the substance I reviewed before, unchanged and still right: drift_line computed from the planted body rather than parsed from the tool's own MISMATCH line; premise: BOTH loops were compared making the pin one-of-two; and the per-bucket negatives being what make compared + armless + interpolated == loops load-bearing rather than satisfiable by filing everything as armless.
Verified here: 6 passed / 24 checks, and the whole corpus green in the container's pinned venv.
Merging is not mine; this is the approval only.
#998 shipped
test/selftest/470with no pytest half.CONTEXT.mdis explicit — "a test that exists in only one harness is not finished" — and neither I nor the reviewer caught it. This is that half.It is not a port, and that is the reason to write it
test/selftest/470test_skip_loop_arms.pyA classifier that filed every site as
armlesswould report zero mismatches, and 470's population premises would still pass on whatever loops remained. So this drives the same tool over planted trees whose right answer is known and asserts the classification itself.The shell says the corpus is clean; this says the thing that reads the corpus can tell the difference. Where they disagree, one of them is wrong — which is the whole argument for two harnesses.
Six tests, seventeen checks
..._naming_its_siblings_arms_is_compared_and_agrees..._rename_in_the_sibling_branch_is_caught..._clean_and_drifted_sites_differ_by_exactly_the_rename..._armless_branch_is_counted_as_armless_and_not_compared..._interpolated_sibling_is_reported_rather_than_compared_wrongly..._every_loop_is_classified_into_exactly_one_categorycompared + armless + interpolated == loopsRemoval proofs
Each mutation was asserted to apply before the run, because a clean pass reads identically whether the code is load-bearing or the edit never landed.
The second is the one worth keeping. Removing the tool's refusal to compare manufactures a false mismatch on a correct site, and a guard that invents a red is the guard people switch off.
Two guards in the tree caught my omissions, which is the argument for both
Neither was told anything; both derived it from the corpus.
The first is the sixth time that arm has decided a membership rather than been told one.
Verified
No shell file changes, so no ledger movement:
check_ledger.tsvcoversharness_selftestandnative_join_runtime_filter, and this adds no shell check.test/selftest/350reads onlyci.ymlnow, so no shell part reads the pytest corpus.What this does not do
armless 0, interpolated 0remains necessary but not sufficient for arming an absence-based gate. That is agreed follow-up work of mine, deliberately not bundled here.🤖 Generated with Claude Code
https://claude.ai/code/session_01EbyGSaU93XYQr8aH4NrUiw