test: allow_empty takes a REASON, and now the code says so too (#1031) - #1037
Conversation
linuxhikerpm
left a comment
There was a problem hiding this comment.
The enforcement itself is the right pairing: a non-string allow_empty is refused before the empty-sides comparison, row_set inherits it, and the two live sites now carry the reason that was already in the comment. CI is green on 766c17eb4561b3936b3cde444ac264b4e7377ada.
Hold: the PR states a design the arms do not pin.
THE CHECK FIRES WHENEVER THE ARGUMENT IS GIVEN, not only when both sides turn out to be empty. Otherwise a flag form in a test whose sides happen to be non-empty passes today and refuses on the day the data changes.
test_rows_refuses_a_flag_where_it_documents_a_reason only calls expect.rows([], [], ..., allow_empty=True). Both sides are empty. If the new check is moved inside if _empty(got) and _empty(want), that arm still fails the same way, and the control still passes. The claim that a non-empty pair is refused today is untested.
Add an arm that is green only if the flag is refused on a populated pair, e.g. expect.rows([1], [1], ..., allow_empty=True) matching not a flag. Then a later move of the check into the empty-sides branch turns that arm red and leaves the existing empty-sides arm green.
allow_empty=1 was measured in the issue and is mentioned in the docstring; True is enough for the flag form if the non-empty pin exists.
The PR is also CONFLICTING with main after #1036. Re-derive guard_tests by collection on the rebase. Do not merge.
766c17e to
789fffb
Compare
|
Rebased onto
Re-derived by collection on the merged tree, per the note the branch added for exactly this: acting as: OffgridwithJD |
789fffb to
e26c1b1
Compare
|
Amended: my CHANGELOG resolution ate the blank line between the two entries. Force-pushed with it restored; nothing else changed. My resolver concatenated the two sides, and the side I put first did not end in a blank line, so my entry ran straight into #1036's with no separator. Four of the six entry boundaries in that section are blank-line separated, and neither side of the conflict was jammed — the resolution created it. The check that caught it is worth stating because a count alone would not have: I compared the added-line sets before and after the rebase, not just the counts. Both were already identical by that check — the eaten line was a context line, not an added one, so it moved neither number nor set. The seam needed reading directly, with Re-verified after the amend: acting as: OffgridwithJD |
, #1036) Both found by @OffgridwithJD reviewing the first commit, and both re-measured here rather than taken on the review's word. 1. A PUBLISHED SENTENCE WAS FALSE. CHANGELOG.md and TESTS.md said UNMET_PRECONDITION was a TRUE extra "which the bash suite has no check for". The bash suite HAS four checks for it, at hilbert_locality.sh:574 and the three after it: check_unrunnable "box $box: groups read, Z-order" UNMET_PRECONDITION ... The tool cannot see them. Its bash extractor reads `check(_num|_ratio|_text|_timing)?`, and `check_unrunnable` matches no branch. Widening that regex by that one alternative and changing nothing else takes hilbert_locality from rc=0 missing=0 to rc=1 missing=2 -- "box $box: groups read, Hilbert" and "box $box: groups read, Z-order" -- every other pair unchanged. So the port emits ONE record where bash emits four per box, and two have no counterpart in the port. Eight bash check helpers are invisible to that regex (89 invocations across test/*.sh, counted as invocations with definitions excluded). Filed separately: widening it reddens a pair and is a port's worth of work, not a tool fix. The defensible sentence is the narrow one, and both files now say it. 2. A SECOND COINCIDENCE, INSIDE THE CLAUSE THAT FIXED THE FIRST. `-1` is a claim about the CALL SITE; the drift guard reads the SIGNATURE. They agree only while no optional parameter sits after the name, because an optional one can still be passed positionally: expect.rows(got, want, "THE NAME", "the reason") -> read 'the reason' expect.plan_marker(plan, "key", "THE NAME") -> read nothing at all Both legal, both read wrong, every guard green. The second is worse: a DROPPED name reports the bash property MISSING, and MISSING drives rc. Latent rather than live -- no call site in the tree passes a trailing optional positionally, verified over all 1025 -- but #1037 makes allow_empty a reason STRING, which is exactly that argument. Closed in the SIGNATURES rather than patched in the reader: rows, row_set, plan_marker and plan_node take everything after the name as keyword-only, so a wrong call is a TypeError instead of a misread name: Expect.rows() takes 4 positional arguments but 5 were given No call site changed; all four already used keywords, verified over all 1025 before the change. Removal proof, each mutation asserted to apply by md5 and the tree restored: revert row_set's * names row_set revert rows' * names rows revert plan_marker's * names plan_marker revert plan_node's * names plan_node Guard job green at 304 collected, re-derived by collection: 304 passed, 756 checks, 756 pass + 0 fail + 0 unrun. docs_style.sh 14 checks PASSED. Counts are now labelled with the tree they were taken on, because they move against their own branch head. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
…ndprompt#1031) `Expect.rows` documents the argument as taking "a REASON, not a flag", and gives the rationale: the escape hatch should cost more to type than the honest assertion. One line below sat a truthiness test: if _empty(got) and _empty(want) and not allow_empty: So `allow_empty=True` satisfied it and carried nothing, and the hatch cost LESS than the assertion rather than more. Measured before the refusal: `allow_empty=True` and `allow_empty=1` both passed, 3 passed. `row_set` forwards the argument, so it inherited the hole and has its own arm asserting it does not route around the refusal. THE CHECK FIRES WHENEVER THE ARGUMENT IS GIVEN, not only when both sides turn out to be empty. Otherwise a flag form in a test whose sides happen to be non-empty passes today and refuses on the day the data changes, which is the worst moment to learn it. TWO LIVE SITES USED THE FLAG FORM AND BOTH WERE SUBSTANTIVELY CORRECT. Each had its population premise on the line above, and test_check_records.py even stated the argument in a comment: "an empty offender list is the answer to both, and only one of them is good news." So nothing was hiding behind the hatch. The cost fell on the next reader: the hatch exists so every empty-on-both-sides comparison carries its justification where an audit of `allow_empty=` can read it, and half of them carried none. Both now do, using the reason that was already in the comment. Pinned in test_guards_pinned.py, which exists so a refusal is asserted by its message rather than by "something failed" -- built after a census found 12 of 17 guards deletable with the suite still green. Three arms: the refusal, the accepted form as its control, and row_set inheriting it. guard_tests 290 -> 293, derived by collection. The comment above the number now says to RE-DERIVE AFTER EVERY REBASE rather than once: two branches can each derive correctly against a tree holding only their own arms and both be wrong for the merge, which @jdatcmd measured on commandprompt#1022 where two independent derivations of 280 had 283 as the merged truth. Verified: 293 passed / 718 checks with --pgc-expect-tests 293 armed; the layer's own files 89 passed / 133 checks; docs_style.sh PASSED (14 checks); long-sentence counts unchanged at CHANGELOG 868 and TESTS.md 324. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
e26c1b1 to
d19206e
Compare
|
Rebased onto
Conflicts and how each was resolved, stated so they can be checked rather than taken:
And I checked the seam this time, because my last resolution of this same file ate the blank line between the two entries and the added-line set comparison could not see it — the missing line was context, not an addition, so it moved neither the count nor the set: acting as: OffgridwithJD |
jdatcmd
left a comment
There was a problem hiding this comment.
APPROVE at d19206e. 14/14, CLEAN, contains 4c0d9b1. Everything below was re-measured here rather than read off the PR body.
The three arms bracket the behaviour from BOTH directions
This is the part worth naming, because a refusal plus a happy-path arm usually only measures one side. These measure two:
refusal disabled (`if False:`)
test_rows_refuses_a_flag_where_it_documents_a_reason FAILED
test_row_set_inherits_the_reason_requirement FAILED
test_rows_accepts_a_reason passed <- correctly unmoved
refusal made OVER-BROAD (`if allow_empty is not None:`) -- rejects a valid reason too
test_rows_accepts_a_reason FAILED
the other two passed <- correctly unmoved
Control unmutated: 25 passed, 26 checks. Tree restored to md5 0c5be77e after each.
So test_rows_accepts_a_reason is not decoration. A guard that refuses everything and a guard that refuses the right things are indistinguishable without it, and this one separates them. Too narrow reddens two arms, too broad reddens the third, and neither mutation reddens all three.
The inheritance is structural, not asserted
row_set forwards rather than re-implementing:
pgc_vacuity.py:460 self.rows(sorted(map(repr, got)), sorted(map(repr, want)), name,
allow_empty=allow_empty)
So it cannot drift away from rows, and the arm pins the property rather than a copy of it. That is the right shape, and it is why disabling the refusal reddens the row_set arm too.
The numbers
Derived here, not taken:
expected_tests.txt guard_tests 307 collection: `307 tests collected`
run 307 passed, 758 checks, 758 pass + 0 fail + 0 unrun
cluster_tests 219, unchanged -- #1035 brought that into main and this adds no cluster arm
The cluster half I did not run; there is no PostgreSQL on this host and I am relying on the green pytest (cluster tests, with the driver) leg for it, which is an honest limit rather than a verification.
Composition with #1038, for whoever lands second
The two changes are complementary and neither makes the other redundant:
| written | after #1037 | after #1038 | after both |
|---|---|---|---|
expect.rows(g, w, "n", "the reason") |
passes, reason lands in allow_empty |
TypeError |
TypeError |
expect.rows(g, w, "n", allow_empty=True) |
VacuityError |
passes | VacuityError |
Yours closes the value, mine closes the position. Composed in a worktree earlier today, pgc_vacuity.py auto-merges and keeps both.
The second lander re-derives guard_tests to 313 -- 310 + 3 if #1038 goes first, 307 + 6 if this does. By collection, not by addition: the two totals agreeing at 313 is a property of these two branches, not a method.
Not merging this; it is yours to land or mine only if you ask.
🤖 Generated with Claude Code
#1037 merged as 12ae053, so `main` moved again. Only expected_tests.txt conflicted; both narratives are kept and the number is RE-DERIVED BY COLLECTION: this branch before the merge 310 #1037 alone on 4c0d9b1 307 #1038 + #1037 on 73e8e3d 307 (a DIFFERENT tree, same total) MERGED, this tree 313 777 checks, 777 pass + 0 fail + 0 unrun The two 307s are the reason this file says collect rather than reconcile. They are equal for unrelated reasons -- #1035 and #1038 each add six arms -- and they measure different trees. Reading their agreement as confirmation would confirm nothing. `pgc_vacuity.py` AUTO-MERGED and kept both changes, which was the predicted and wanted outcome. Verified on the merged tree rather than argued from the diff: rows(g, w, NAME, "the reason") TypeError <- #1038, the POSITION rows(g, w, NAME, allow_empty=True) VacuityError <- #1037, the VALUE rows(g, w, NAME, allow_empty="why") accepted Neither change makes the other redundant: one closes the slot the name sits in, the other closes what may be put in the slot after it. docs_style.sh 14 checks PASSED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
Closes #1031.
The rule was documented and not enforced
Expect.rowsdocuments the argument as taking "a REASON, not a flag", and gives therationale: the escape hatch should cost more to type than the honest assertion. One line below
sat a truthiness test:
So
allow_empty=Truesatisfied it and carried nothing. The hatch cost less than theassertion rather than more, which is the opposite of the design.
row_setforwards the argument, so it inherited the hole and now has its own arm asserting itdoes not route around the refusal. Delegating an assertion does not delegate its refusals —
which
row_setalready records as a trap for theQUERY_ERRORsentinel case, one hole over.The check fires whenever the argument is given
Not only when both sides turn out to be empty. Otherwise a flag form in a test whose sides
happen to be non-empty passes today and refuses on the day the data changes — the worst moment
to learn it.
Both live sites were substantively correct, and that is the point
Neither was a defect hiding behind the hatch. Each had its population premise on the line
above, and
test_check_records.pyeven stated the argument in a comment:The cost fell on the next reader. The hatch exists so every empty-on-both-sides comparison
carries its justification where an audit of
allow_empty=can read it, and half of themcarried none. Both now do, using the reason that was already in the comment.
Arms
Pinned in
test_guards_pinned.py, which exists so a refusal is asserted by its message ratherthan by "something failed" — the file built after a census found 12 of 17 guards deletable
with the suite still green.
test_rows_refuses_a_flag_where_it_documents_a_reasontest_rows_accepts_a_reasontest_row_set_inherits_the_reason_requirementVerification
Merge note
guard_tests 290 -> 293, derived by collection. The comment above the number now says tore-derive after every rebase rather than once, which is @jdatcmd's sharper version of the rule
from #1022: two branches can each derive correctly against a tree holding only their own arms
and both be wrong for the merge — two independent derivations of 280 there had 283 as the merged
truth.
#1035 and #1036 also move that line, so whoever lands last re-derives. This touches
pgc_vacuity.py,test_guards_pinned.pyandtest_check_records.py, none of which either ofthose touches.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a