test: the INCOMPLETE path must run whole, and the count is per major (#858) - #874
Conversation
Reproduced independently: every number in the summary holds, and so does the controlI ran this rather than read it. Separate container ( The removal proof reproduces228 -> 261 is your +33, arrived at independently. Both mutation counts match your The commit-1 arm is the one worth quoting, because the red states the bug rather
I verified the premise separately in the source rather than taking it from the The control that matters, run hereYour strongest claim is the one a green matrix cannot distinguish from a broken My skip set is 5 where yours is 9 — different optional dependencies in this One ask: the CHANGELOGTwo commits, one of them a real counting bug in the runner, and no That is the only thing I would hold this for. Two smaller notes
Not approving: same account as the author. This is a comment, and the merge is |
78ce643 to
af72741
Compare
`suites_incomplete=${suites_incomplete:-0}` is a `set -u` guard, not an
initialiser. It keeps whatever the previous major left, while `suites_ran` and
`suites_skipped` on the two lines above it are zeroed unconditionally, and
`verfail` is reset per major as well.
So on the five-major matrix the count would accumulate: PG16 would report
PG15's incomplete suites in its own summary line, and still print PASS, because
the only per-run thing in a per-major report was the number that exists to say
a check could not be evaluated.
Latent today. `check_unrunnable` has no production call site, so no real suite
can reach the INCOMPLETE state in a matrix run yet. It stops being latent the
moment one does, which is what phase 2 of #858 is for.
Found behaviourally rather than by reading: driving the runner's own collect
loop twice counted 2 incompletes where 1 had occurred. The arm that does the
driving lands in the next commit, along with the extraction that makes the loop
drivable at all; the removal proof for this line is the mutation that puts
`${suites_incomplete:-0}` back and reddens two named checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDbfRym2V1sYFmMZ5gnsQL
…ted (#858) #859's regression was not in `pgc_classify_suite_rc`. The classifier returned INCOMPLETE correctly and the CALLER threw the answer away into a write-only flag. Its fifteen arms could not have caught that, because the caller was four branches in the middle of the per-major loop, and a loop that needs a suite list and a populated build directory is not something a selftest can drive. So the untestable thing is extracted. `pgc_tally_suite NAME VERDICT LOGFILE` takes the four branches out of the loop, which becomes three lines and a call. No behaviour changes: the same counters, the same stdout, the same statement the mapping is called by. `$s` becomes argument 1 and `$builddir/${s}.log` becomes argument 3, because the selftest has neither. The function must never declare the six caller counters `local`, and says so in its own comment. The mutation that adds `local` reddens thirteen checks. Selftest 330 then runs the chain in five links, none of them stubbed and none of them re-derived. Every function and every block is lifted out of run_all_versions.sh by text, and every extraction is premised on being non-empty before it is evalled, because `eval ""` succeeds silently. Link 4 is the one that earns the file. Everything else in 330 stays green if the runner defines `pgc_tally_suite` and never calls it, so link 4 evals the loop itself and reads its own text for the delegation. A first draft of this file had links 1 to 3 and would have merged a test that passes over dead code -- the defect class the file exists to prevent, committed by the file itself. Four weaknesses in that draft are fixed here. The last arm re-derived the runner's PASS/FAIL rule inside the test instead of evalling the runner's own branch. The control asserted three values it had itself just assigned. The skip arm could not tell "left alone" from "zeroed", so it now starts at 5. And the verdict handed to the tally was the string INCOMPLETE retyped, which cut the chain at the exact joint the file exists to test; it is now the classifier's own output. One stale comment corrected in passing: the SKIP branch said "Exit 2" where the classifier tests 66. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDbfRym2V1sYFmMZ5gnsQL
…ry (#858) This repo gives test-only fixes their own entries -- #852, #854 and #856 all have one -- and the per-major reset is a real runner bug even though it is latent while check_unrunnable has no production call site. The entry states the failure a reader would have seen (PG16 reporting PG15's incomplete suites and still printing PASS) rather than the line that changed, and says why the tally became a function: nothing could reach the caller while it was four branches inside a loop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDbfRym2V1sYFmMZ5gnsQL
af72741 to
57d8db7
Compare
jdatcmd
left a comment
There was a problem hiding this comment.
Approved at 57d8db7.
Zero blocking findings from the adversarial pass. The thing that earns this file is the one
the author already names: link 4 reads the runner's own collect loop, so an extraction
that defines pgc_tally_suite and leaves the old inline branch in place fails here rather
than passing over dead code. That is the exact defect class #859 shipped — a correct answer
computed and discarded — and testing the function without its caller would not have caught it.
On the rebase. This branch was af72741 when I reviewed it; #868 landed and it went
CONFLICTING on CHANGELOG.md, so I rebased it onto a26c2ae myself and kept both entries.
The check that my approval still covers the code is not "I looked at the diff":
tree I built and tested 1f05ffb210ccd41d29cd42ae584156d631b559fd
rebased branch tree 1f05ffb210ccd41d29cd42ae584156d631b559fd
Identical tree oids. The bytes I ran the matrix on and the bytes being merged are the same
bytes. (The content-diff md5 does differ across the rebase, but only because the diff is taken
against a different base; tree identity is the stronger statement and supersedes it.)
Composition, not the branch, since two green branches are not evidence that they compose:
full PG 17.10 matrix on the merged tree against main b4f0a456 in a private prefix.
| verdicts | PASS | SKIP | FAIL | |
|---|---|---|---|---|
main b4f0a45 |
239 | 234 | 5 | 0 |
| main + #871 + #868 + #874 | 241 | 236 | 5 | 0 |
Baseline − composed is empty. inputs == sum(buckets) on both sides, counted from the run
output rather than retyped. CI is green at 57d8db7 on all 12 legs — I waited for it rather
than merging on the tick af72741 had earned, because a green tick that belongs to a commit
which is no longer the head is the trap this board has already been caught by once.
Follow-up to #859. Two commits: a latent counting bug in the runner, then the
extraction that makes the INCOMPLETE dispatch testable end to end.
Why this exists
#859 shipped fifteen arms that drive
pgc_classify_suite_rcandpgc_verdict_fails_majordirectly. Its regression was not in either function.The classifier returned INCOMPLETE correctly and the caller threw the answer
away into a write-only flag. Nothing could reach the caller, because the
caller was a branch in the middle of a loop that needs a suite list and a
populated build directory before it will run at all.
So the untestable thing got extracted.
pgc_tally_suite NAME VERDICT LOGFILEtakes the four branches out of the per-major loop; the loop becomes three lines
and a call. Selftest 330 then drives the whole chain: a real suite exits 67, the
runner's own classifier reads the
.rcand.logthat suite produced, therunner's own tally consumes the classifier's verdict, the runner's own collect
loop runs over both fixtures, and the runner's own major-verdict branch decides
PASS or FAIL. Nothing is stubbed and nothing is re-derived — every function and
every block is lifted out of
run_all_versions.shby text.Commit 1: the incomplete count was per matrix, not per major
suites_incomplete=${suites_incomplete:-0}is aset -uguard, not aninitialiser. It keeps whatever the previous major left, while
suites_ranandsuites_skippedbeside it are zeroed unconditionally andverfailis permajor. On the five-major matrix PG16 would report PG15's incomplete suites in
its own summary line and still print PASS.
Latent today, because
check_unrunnablehas no production call site. It stopsbeing latent the moment one appears.
Found behaviourally, not by reading: 330 drives the collect loop twice, and the
second drive counted 2 incompletes where 1 had occurred.
The removal proof
Fourteen arms, each in its own tree, each asserting its mutation applied by md5
before the run, none of them installing into a shared prefix. Predictions were
written down before any arm ran.
verfail=1from the INCOMPLETE armsuites_incompleteincrementsuites_ranincrementresults+=linelocalon the six caller countersoverall=1The row that matters is the dead-function arm. With
pgc_tally_suitedefinedand never called, every behavioural arm in 330 stays green — they cannot see
dead code. Only the two arms that read the loop's own text go red. That is the
arm the first draft of this file did not have, and without it this PR would
have merged a test that passes over code the runner never runs.
The full matrix, and the control that matters
The refactored code decides every suite's verdict, so a defect in it could turn
every FAIL into a PASS and a green matrix would look identical to a correct one.
Three PG17 runs over all 237 suites, on three identically prepared git
checkouts, each with its own prefix, lock and port seed:
PASS PG17 (228 ran, 9 skipped, 0 incomplete)origin/mainat 53224e4PASS PG17 (228 ran, 9 skipped, 0 incomplete)FAIL PG17 (228 ran, 9 skipped, 0 incomplete)All 237 per-suite verdicts are identical between this branch and
origin/main-- set difference empty both ways. The injected run differs from this branch in
exactly one entry,
wal_envelope=PASSbecomingwal_envelope=FAIL, and namesthe injected check as the reason.
Selftest counts:
origin/main228 checks, this branch 261. The 33 addedreconcile against the 33
checkcalls selftest 330 runs itself; a naive grepsays 35 because two
checklines live inside the fixture heredocs the filegenerates.
Commit 1 alone was run separately and is green (228 passed, 0 failed, exit 0),
so there is no red commit to bisect through.
What I got wrong, recorded rather than tidied away
Three of my own errors, all caught by controls rather than by a result looking
wrong:
.git, so eight checks in selftest 310reported
no-repo. I read that as a result before noticing it was myinstrument. Redone as a real clone.
overall=1mutation matched 8 sites, not 1. The arm refused to run andreported
mutation_applied=falserather than emitting a verdict — which isthe whole point of asserting the mutation applied.
anchors its extraction on, so the premise fired and the downstream arms were
inconclusive. Replaced with a mutation of the branch body. The premise doing
that is the anti-drift guard working, not a defect.
I also predicted the dead-function arm would redden "nothing else". It reddens
320's pinned call site too, because reverting the loop leaves the literal in
both the loop and the function, taking the count from 1 to 2. That arm catches
a duplicated call as well as a missing one.
What this does NOT cover
check_unrunnablestill has zero production call sites. The state shipsunused and cannot appear in a real matrix run. Phase 2 of A check has no way to say it could not run, and nothing records whether a check has ever been red #858 is unchanged.
the classifier tests 66.
Provenance of the numbers
Everything above was run in the Incus container
pgcolumnar-devon PG 17.10,in a lane of its own (
/usr/local/pg17_e858*) that installs nothing into theshared prefix. The mutation arms ran with
PGC_SKIP_BUILD=1against onepre-built object, so no arm could overwrite another's binary -- the failure mode
that caused last week's retraction.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WDbfRym2V1sYFmMZ5gnsQL