test/pytest: a write that wrote no rows is not a fixture (#432) - #935
Conversation
…pt#432) `INSERT ... SELECT ... WHERE false` writes nothing and raises nothing. psycopg reports `INSERT 0 0` with a `rowcount` of 0, and nothing in the corpus read either field -- so the fixture a test meant to build did not exist, and every assertion below it compared two empty things. That is `insert-wrote-no-rows`, entry 2 on VACUITY_MODES.md's own list of what to add next. Measured before building: 12 write statements across 5 files, and `rowcount` read at exactly one site, which does not assert on it. THE COMMAND TAG DECIDES, NOT THE ROW COUNT. `SELECT 0` and `INSERT 0 0` both carry `rowcount == 0`, so a guard keyed on the count alone would refuse every test whose last statement was a SELECT over an empty result -- a legitimate and common assertion. `statusmessage` is the server's own command tag, so this guard never parses SQL. Measured on PG 18 against a pgcolumnar table: statusmessage rowcount statement CREATE TABLE -1 CREATE TABLE t (i int) USING pgcolumnar INSERT 0 5 5 INSERT INTO t SELECT g FROM generate_series(1,5) g INSERT 0 0 0 INSERT ... WHERE false UPDATE 0 0 UPDATE t SET i = i WHERE i > 100 DELETE 0 0 DELETE FROM t WHERE i > 100 SELECT 0 0 SELECT * FROM t WHERE false SET -1 SET search_path TO public TRUNCATE TABLE -1 TRUNCATE t A DELIBERATE ZERO STAYS WRITABLE. A DELETE that must match nothing is a real negative control, so `expect.wrote(cur, 0, name)` both compares the count and marks the write as named. An unnamed zero fails the test; naming a count does not excuse a wrong one; and a `rowcount` of -1 is refused rather than compared, for the reason `expect.rowcount` already records. IN THE CALL PHASE, NOT A TEARDOWN. commandprompt#931 measured that a guard run as a teardown fixture reports the test it guards as PASSED and fails separately, so a reader sees a green test beside an error. TWO PROPERTIES, TWO FILES, AND THE SPLIT IS MEASURED RATHER THAN ASSERTED. The classifier and the refusal live in test_writes_wrote_rows.py, which needs no database: a stub cursor carrying the two measured fields exercises them exactly. Whether the connection the tests actually use is watched is a different claim that no driver-free arm can make, and test_the_connection_the_tests_use_is_watched makes it through a real `INSERT ... WHERE false`, on both `conn.execute` and a cursor the connection handed out -- 24 and 42 sites in the corpus, so a proxy watching only the connection would leave most of it unwatched. Unwiring the conftest proxy and changing nothing else leaves the driver-free file at 10 passed and reds the wiring arm alone. That is commandprompt#917's defect in miniature: its pytest twin tested the reconciler's body and left the runner's CALL to it uncovered, so removing the call kept the pytest half green at 9 passed while the shell half went red by one. PROVE BY REMOVAL, five mutations, each applied by exact string match with the file asserted to still parse: control free 10 passed wiring 9 passed no refusal free 2 failed wiring 9 passed command tag ignored free 2 failed wiring 1 failed every statement is a write free 4 failed wiring 1 failed connection not wrapped free 10 PASSED wiring 1 failed acknowledgement not recorded free 1 failed wiring 1 failed FALSE-POSITIVE BUDGET FIRST, because a guard that reddens a legitimate write is worse than the mode: the full corpus with a cluster is 249 passed, and none of the 12 write sites already in the corpus reddened. TWO OF MY OWN ARMS WERE WRONG, recorded because each produced a green that meant nothing: * An arm asserting only `failed=1` passed before the feature existed. The inner test called a function not yet written, got an AttributeError, and the outer arm read that as the comparison failing. Naming the numbers is what makes the red the right red. * A multi-word pattern can straddle pytest's word wrap. `expect.refusal` anchors each pattern to one `E` line and pytest wraps a long traceback line, so matching "wrote no rows" failed against a message containing it -- which reads exactly like "the guard did not fire". The refusal now leads with the mode's own kebab-case id, which is one token and cannot be split. Docs carried in the same commit, per jd's rule. VACUITY_MODES.md moves the mode to section 2 and keeps a back-reference where it was, and its stated totals go 27 -> 28 refused and 45 -> 44 not refused -- checked by the corpus's own arms, which is how I learned the numbers rather than deciding them. TESTS.md gains section 22, and the paragraph in section 18 that listed this gap is rewritten rather than deleted, because a reader who knew the gap needs to find out where it went. Gate on this tree: harness_selftest 588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED driver-free job 10 files, 171 passed, psycopg absent from the venv full corpus 249 passed with a cluster on pg18a Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
commandprompt#432) Both were found by asking what the new guard ACCEPTS rather than what it refuses, and the first version was green with both. `expect.wrote` ACCEPTED A SELECT. A query matching nothing reports `SELECT 0` with `rowcount == 0`, so `expect.wrote(cur, 0, name)` compared 0 with 0 and passed -- asserting "this write wrote no rows" about a statement that is not a write. It reads as a deliberate zero and pins nothing, which is this layer's own subject appearing inside the assertion written to close it. A tag outside the write vocabulary is now refused, with `not-a-write` as the token. THE REFUSAL NUMBERED THE WRONG THING. It enumerated the empty writes it was about to print, so `commandprompt#1` meant "the first one I am complaining about" and identified no statement: a reader counting writes in the source went to the wrong line. The ordinal is now the write's position among ALL the test's writes, assigned when it is recorded. AND THAT ONE PRODUCED AN ARM THAT COULD NOT DISCRIMINATE, which is the part worth keeping. My first version of the arm used two writes, where the old numbering and the new one both print `commandprompt#1` -- so it passed against the defect it was written for. It now uses three writes with the empty one SECOND: the filtered numbering says `commandprompt#1`, the absolute one says `commandprompt#2`, and only one of those sends a reader to the right statement. An acknowledgement is now matched by the CURSOR that ran the statement rather than by `(tag, count)`. Two writes can carry the same tag and the same count -- one accidental, one deliberate -- and matching on the pair acknowledged whichever came first, marking the accidental one as named and reporting the deliberate one instead. The write is stamped on the cursor in `note_write`, defensively, because a psycopg cursor may refuse a new attribute; the value match stays as the fallback, which is why the ordinal had to become absolute rather than relative to the survivors. Prove by removal, now eight mutations, each applied by exact string match with the file asserted to still parse: driver-free wiring arm control 13 passed 9 passed no refusal 3 failed 9 passed command tag ignored 2 failed 1 failed every statement is a write 3 failed 1 failed connection not wrapped 13 PASSED 1 failed acknowledgement not recorded 1 failed 9 passed non-write accepted 1 failed 9 passed acknowledge by (tag, count) only 1 failed 9 passed ordinal relative to survivors 1 failed 9 passed The fifth row is still the one that matters: unwiring the connection leaves every driver-free arm green and reds the wiring arm alone, which is why they are two files. Gate: harness_selftest 588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED driver-free job 10 files, 174 passed, psycopg absent from the venv full corpus 252 passed with a cluster on pg18a Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
|
Pushed
|
| mutation | driver-free | wiring arm |
|---|---|---|
| control | 13 passed | 9 passed |
| no refusal | 3 failed | 9 passed |
| command tag ignored | 2 failed | 1 failed |
| every statement treated as a write | 3 failed | 1 failed |
| connection not wrapped | 13 passed | 1 failed |
| acknowledgement not recorded | 1 failed | 9 passed |
| non-write accepted | 1 failed | 9 passed |
acknowledge by (tag, count) only |
1 failed | 9 passed |
| ordinal relative to the survivors | 1 failed | 9 passed |
Row five is still the one that matters: unwiring the connection leaves every driver-free arm green and reds the wiring arm alone, which is why they are two files.
Gate
harness_selftest 588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED
driver-free job 10 files, 174 passed, psycopg absent from the venv
full corpus 252 passed with a cluster on pg18a
Two things I would most like attacked
Since I found these by attacking rather than by running:
- Whether
statusmessagecan be absent or reworded on a path this corpus uses. I measured PG 18 only. The guard returns early on a falsystatusmessage, so the failure direction is a silent non-record rather than a false red — which is the worse direction for a guard. executemany. It reports the count of the last batch, and the corpus has no call sites today, so the arm for it would be an instrument with nothing exercising it. I recorded the path rather than guessing its semantics.
jdatcmd
left a comment
There was a problem hiding this comment.
Requesting changes at 29834e3b. You found both of my findings before I could post them, and the third one you found on your own — wrote() passing on a SELECT 0 — I had missed entirely. But the identity fix does not work against the real driver, and it takes the ordinal fix down with it.
Blocking: the identity path cannot run, and its arms cannot see that
note_write stamps self._cur, the raw psycopg cursor. A psycopg cursor cannot take a new attribute, so the stamp is swallowed by the except (AttributeError, TypeError): pass on every real write:
connection.cursor() stampable=FALSE (Cursor: AttributeError)
conn.execute() return stampable=FALSE (Cursor: AttributeError)
ServerCursor stampable=FALSE (ServerCursor: AttributeError)
And the object the test holds is a _WatchedCursor, whose __getattr__ forwards _pgc_write to the raw cursor, which never got one.
End to end against a real PostgreSQL 17, two identical zero-row INSERTs through _WatchedConnection, naming the second:
writes recorded: [(1, INSERT, 0), (2, INSERT, 0)]
real cursor carries stamp? False
what the test holds: _WatchedCursor
getattr(c2, '_pgc_write'): None <- falls back to value matching
write #1 acknowledged=True <- the one I did NOT name
write #2 acknowledged=False
-> refusal names statement [2]; the unnamed one is #1
So wrote() still matches by value, exactly as before the commit — and the absolute ordinal now points at the wrong statement in precisely the case it was added for. The two fixes are load-bearing on each other: the ordinal is only trustworthy when the acknowledgement is by identity, and it never is.
The arms cannot catch this because the stub is stampable and the real cursor is not:
the arms' _Cur stub stampable=True
a real psycopg Cursor stampable=FALSE
13 passed. They prove the identity mechanism on an object that differs from the real one in exactly the respect under test — which is the shape your own _WatchedCursor docstring warns about, a proxy standing in for a thing that behaves differently.
The fix looks small: stamp the object the caller receives rather than the one psycopg owns. execute already returns self, and _WatchedCursor is an ordinary object with a __dict__, so stamping the proxy lands and __getattr__ never intercepts an attribute the instance actually has. What is worth more than the fix is an arm that would have caught it — the cluster-bound wiring test is the natural home, since it is the one place a real cursor exists, and it is already the arm that exists to prove the call site rather than the function.
The verdict itself is not wrong today. I checked separately: because value matching requires equal tag and equal count, a mis-assignment can only happen between indistinguishable writes, so the refusal still fires the right number of times. What breaks is which statement the message names.
What I verified and could not fault
Your first question, answered wider than you asked. You measured PG 18; I measured PG 15.18 and PG 17.10, twelve shapes each, through psycopg 3.3.5. statusmessage is never absent and the wording is byte-identical across both:
INSERT 0 0 / UPDATE 0 / DELETE 0 / MERGE 0 rowcount 0 guarded
SELECT 0 rowcount 0 NOT guarded
CREATE TABLE rowcount -1 NOT guarded
Your central design claim, proven by mutation rather than argued. Removing the tag check:
with the tag count-keyed
SELECT over empty result recorded=0 refuse=False recorded=1 refuse=TRUE
zero-row INSERT recorded=1 refuse=True recorded=1 refuse=True
The count-keyed version refuses a legitimate SELECT over an empty result. The tag is the right key, and not-a-write closing the wrote() side of the same hole is the better half of this commit — a SELECT 0 compared 0 with 0 and passed, asserting "this write wrote no rows" about a statement that is not a write. I did not find that one.
The ordinal fix is correct on the identity path. Four writes, only the third empty: the message now says #3 INSERT where it used to say #1.
Four shapes write rows and escape the tag, measured on both majors — a data-modifying CTE and a SELECT of an inserting function both report SELECT, a DO block reports DO, a CALL reports CALL. Zero occur in the corpus (0 files for each; the corpus writes are 15 INSERT, 14 COPY, 2 DELETE, 1 UPDATE), so this is a residual to state rather than a gap to close.
Doc arithmetic 53/53, the new file declared, membership_report [], cluster-free 11 = job runnable 10 + driver-dependent 1, driver-free job 10 files / 171 passed.
The wiring arm remains the best part, and it is the #917 lesson applied by name — "proving a function and proving its call site are two proofs, and the second is the one that goes missing." This finding is that sentence coming true one level down: the stamp is a call site too.
@jdatcmd found that the identity fix could not run against the real driver, and that it took the ordinal fix down with it. Both are right and the second is the part that mattered: the two rest on each other, so with the stamp swallowed the refusal named the wrong statement in exactly the case the ordinal was added for. `note_write` stamped the RAW psycopg cursor. A `psycopg.Cursor` cannot take a new attribute, so the stamp was swallowed by its own `except` on every real write, and the object the test holds is a `_WatchedCursor` whose `__getattr__` forwards the lookup to the raw cursor that never got one. Measured through the driver: connection.cursor() stampable=False (Cursor: AttributeError) conn.execute() return stampable=False (Cursor: AttributeError) ServerCursor stampable=False (ServerCursor: AttributeError) the arms' _Cur stub stampable=True THE ARMS COULD NOT SEE IT, and that is the lesson rather than the bug. They proved the identity mechanism on an object that differs from the real one in exactly the respect under test -- the hazard `_WatchedCursor`'s own docstring names, arriving in the arms written to guard it. The fix stamps the object the CALLER receives; the stamp on the raw cursor stays, because it is what the layer's own stub-driven arms use, and its comment now says what it reaches and what it does not. AND MY FIRST ARM FOR IT PINNED NEITHER SITE. There are two stamps -- one in `_WatchedCursor.execute`, one in `_WatchedConnection.execute` -- and the arm exercised only the connection path, so removing the cursor-path stamp left it green. Found by mutating the fix rather than by reading it. The arm now drives both paths with INDISTINGUISHABLE writes, which is the only case where identity and value matching can disagree: the existing wiring arm uses the cursor path with different counts, where value matching finds the right write whether or not the stamp lands. Prove by removal, ten mutations, each by exact string with a parse assertion: driver-free cluster arms control 13 passed 10 passed no refusal 3 failed 10 passed command tag ignored 2 failed 2 failed every statement is a write 3 failed 2 failed connection not wrapped 13 passed 2 failed acknowledgement not recorded 1 failed 1 failed non-write accepted 1 failed 10 passed acknowledge by (tag, count) only 1 failed 1 failed ordinal relative to survivors 1 failed 10 passed cursor-path stamp removed 13 passed 1 failed connection-path stamp removed 13 passed 1 failed Three rows now red ONLY the cluster side, which is the argument for the split stated three times over: a driver-free arm cannot see wiring, and cannot see an attribute a real object refuses. RESIDUAL RECORDED, MEASURED BY @jdatcmd ON TWO MAJORS. `statusmessage` is never absent and its wording is byte-identical on PG 15.18 and PG 17.10 across twelve shapes. Four shapes write rows while reporting a non-write tag -- a data-modifying CTE and a SELECT of an inserting function both report SELECT, a DO block reports DO, a CALL reports CALL -- and ZERO occur in the corpus, whose writes are 15 INSERT, 14 COPY, 2 DELETE and 1 UPDATE. So it is stated rather than closed: an arm for a shape nothing uses is an instrument with nothing exercising it. Gate: harness_selftest 588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED driver-free job 10 files, 174 passed, psycopg absent from the venv full corpus 253 passed with a cluster on pg18a Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
|
Fixed at The stamp now lands on the object the caller receives. The stamp on the raw cursor stays, because it is what the layer's stub-driven arms use, and its comment says what it reaches and what it does not rather than implying it works everywhere. And my first arm for it pinned neither siteI wrote the cluster-bound arm you suggested, it went red before the fix and green after, and it was still wrong: there are two stamp sites — Found by mutating the fix rather than by reading it, which is the only reason I know. The arm now drives both paths with indistinguishable writes, since that is the only case where identity and value matching can disagree — the existing wiring arm uses the cursor path with different counts, where value matching finds the right write whether or not the stamp lands. That is why it never caught this. Ten mutations, and three of them red only the cluster side
Rows 5, 10 and 11 are the argument for the split stated three times: a driver-free arm cannot see wiring, and it cannot see an attribute that a real object refuses. Your residual is in the docs, attributed
Your mutation of my central claim is better evidence than my prose for it. I argued the tag is the right key; you showed the count-keyed version refuses a legitimate GateOne thing still worth attacking, since you found the last one by asking what the arms could not see: the acknowledgement now depends on the proxy being the object a test passes to |
jdatcmd
left a comment
There was a problem hiding this comment.
Approving at 7c5d85b5. My finding is closed, and I verified it the way it was found — against a real PostgreSQL 17 through _WatchedConnection, not against stubs.
The stamp lands now, on both paths:
via conn.execute stamp=yes refusal names [1] -> correct
via cursor.execute stamp=yes refusal names [1] -> correct
Naming the second of two indistinguishable zero-row writes acknowledges the second, and the refusal names statement #1 — the genuinely unnamed one. Before, the stamp was None, naming the second acknowledged the first, and the refusal named #2. Stamping the proxy rather than the cursor psycopg owns is the right fix, and initialising self._pgc_write = None in __init__ so absence is distinguishable from a cursor that has not run is the detail that makes getattr(..., None) honest.
Your own finding reproduces, and both sites are independently load-bearing. I removed each stamp in turn, leaving note_write called so only the stamp is under test:
stamp removed at 798 (cursor path) conn path correct cursor path WRONG, names [2]
stamp removed at 834 (connection path) conn path WRONG cursor path correct
So each site is observable only on its own path — which is exactly why an arm driving one of them pinned neither. Worth stating for the record: all three stamp removals leave the driver-free file at 13 passed, because the stub is stampable so identity works there regardless. The cluster side is the only place this property exists at all.
The new arm drives both paths with indistinguishable writes, asserts the stamp is a _Write, asserts its absolute ordinal is 2, and asserts acknowledgement lands on the second while leaving the first. The comment records why the pre-existing wiring arm could not have caught it — it uses the cursor path with distinguishable counts, where value matching finds the right write whether or not the stamp lands. That sentence is the useful part of the whole change.
not-a-write is the finding I missed and it is the better half of the earlier commit: wrote() on a SELECT 0 compared 0 with 0 and passed, asserting "this write wrote no rows" about a statement that is not a write — this layer's own subject appearing inside the assertion meant to close it.
My residual is recorded accurately, including the measurement and its limits: four shapes write rows and report a non-write tag, zero occur in the corpus, and "writing an arm for a shape nothing uses would be an instrument with nothing exercising it" is the right reason not to close it.
State at this head: 13 arms passed, the driver-free job 10 files / 174 passed, membership_report [].
Taking your _comparisons() question, since you asked me to choose the shape
You are right that wrote(cur, want, name) is excluded correctly but by accident — the derivation keys on the first two parameter names, and a future assertion whose first parameter is not called got would be excluded just as silently.
I do not think a declared exclusion list is the shape, though, because it is a hand-maintained list of names and this layer's whole argument against _WRITE_TAGS-style lists is that they go stale invisibly. What I would rather have is the same trick you used for the write tags: make the exclusion derivable and then assert the partition. Every public method lands in exactly one of selected / excluded, each excluded one carries a stated reason next to its signature, and an arm prints inputs == selected + excluded and fails when a method appears that matches neither rule. That converts "excluded because nobody named a parameter got" into "excluded because its left side is a cursor, and here is the arm that says so".
That is #937-adjacent work rather than this PR's, and it is your file as much as mine now, so I will open it as its own issue unless you would rather carry it.
#926 and #935 landed while this waited. CHANGELOG: both sides append at the top of the same section and neither replaces anything, so the union is the resolution. TESTS.md: one region, and this side of it is EMPTY. This branch deleted its section 22 in aa07d47, and main added its own -- #935's test_writes_wrote_rows.py. Main's is the only content, so the resolution is to take it. 22 headings against 22 TOC entries, every anchor equal to GitHub's derivation, contiguous 1..22. selftest 080 goes from 15 checks to 53 because #926 landed, which is the number that branch reports and not a change of this one. On the composed tree: 350 53/53, 400 81/81, 080 53/53, shellcheck rc=0 over the whole harness, the driver-free job 10 files 174 passed, membership_report []. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
#926 and #935 landed on main and #923 dropped its coupled pytest twin, so all three reached this branch at once. NO_CLUSTER: the base removed test_check_results_are_machine_readable.py, this branch had added test_mutation_ledger.py, and the conflict spanned both. Kept the ledger entry and dropped the deleted file's. Its comment block was OUTSIDE the conflict region and survived as six orphaned lines above an unrelated entry -- removed, and the module asserted to still parse and to hold 11 entries with the deleted file absent. That is the orphaned-heading shape this repository has been bitten by before, and git will not point at it. TESTS.md: two regions. In the TOC this branch had 22 (the deleted file) and 23 (the ledger) while main had its own 22, test_writes_wrote_rows.py; in the body this side opened with six orphaned lines of the deleted section before the ledger's. Composed as main's 22 followed by this branch's 23. 23 headings against 23 TOC entries, every anchor equal to GitHub's derivation, contiguous 1..23. THE LEDGER IS REGENERATED AGAIN, and #926 is why: it took selftest 080 from 15 checks to 53, and the gate refuses a check it has never seen. From a real run of the composed tree: harness_selftest.sh: PASSED, rc=0 checks run: 773 | accounting: 773 passed + 0 failed + 0 unrunnable + 0 skipped ledger: 734 rows -> 772 | never=772, ever red=0 log triples not in the ledger: 0 All 772 rows carry five fields, none ends in a tab, and the budget's asserted census follows to 772. Gates on the composed tree: 350 53/53, 400 81/81, 410 96 checks 0 failed, 080 53/53, shellcheck rc=0, the driver-free job 11 files 183 passed, membership_report []. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
# Conflicts: # test/pytest/VACUITY_MODES.md
…her branch #935 closed `insert-wrote-no-rows` and moved the refused total 27 -> 28. This branch closes `raises-catches-setup` and moved it 27 -> 28 as well. The merge was CLEAN and therefore wrong: git took one 28 where the answer is 29, because the number is a property of both closures rather than of either. That is the exact failure TESTS.md's own header records about counts in this document -- "a claim whose correct value is a function of the MERGE rather than of either branch, so it collided on essentially every rebase" -- and the reason the counts are checked by arms instead of trusted. The arms named every number: refused today 28 -> 29 not refused 44 -> 43 demonstrated 48 -> 47 (section 3's sentence) TESTS.md prose 28 -> 29, and 44/43 -> 43/42 README.md 28 -> 29 I did not decide any of them; each one is what `test_the_prose_totals_match_the_counted_modes` and its siblings reported against the ids on disk. Gate on the merged tree: harness_selftest 588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED driver-free job 10 files, 174 passed, psycopg absent from the venv full corpus 257 passed with a cluster on pg18a the raises scan zero offences over the corpus, all 9 compound kinds recognised Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
INSERT ... SELECT ... WHERE falsewrites nothing and raises nothing. psycopg reportsINSERT 0 0with arowcountof 0, and nothing in the corpus read either field — so the fixture a test meant to build did not exist, and every assertion below it compared two empty things.That is
insert-wrote-no-rows, entry 2 onVACUITY_MODES.md's own list of what to add next.Measured before building: 12 write statements across 5 files, and
rowcountread at exactly one site, which does not assert on it.The command tag decides, not the row count
SELECT 0andINSERT 0 0both carryrowcount == 0. A guard keyed on the count alone would refuse every test whose last statement was a SELECT over an empty result — a legitimate and common assertion.statusmessageis the server's own command tag, so this guard never parses SQL. Measured on PG 18 against a pgcolumnar table:A deliberate zero stays writable
A DELETE that must match nothing is a real negative control, so
expect.wrote(cur, 0, name)both compares the count and marks the write as named. An unnamed zero fails the test; naming a count does not excuse a wrong one; and arowcountof-1is refused rather than compared, for the reasonexpect.rowcountalready records.The refusal runs in the call phase, not a teardown fixture — #931 measured that a teardown guard reports the test it guards as PASSED and fails separately.
Two properties, two files, and the split is measured
The classifier and the refusal live in
test_writes_wrote_rows.py, which needs no database: a stub cursor carrying the two measured fields exercises them exactly.Whether the connection the tests actually use is watched is a different claim, and no driver-free arm can make it.
test_the_connection_the_tests_use_is_watchedmakes it through a realINSERT ... WHERE false, on bothconn.executeand a cursor the connection handed out — 24 and 42 sites in the corpus respectively, so a proxy watching only the connection would leave most of it unwatched.Unwiring the conftest proxy and changing nothing else leaves the driver-free file at 10 passed and reds the wiring arm alone. That is #917's defect in miniature: its pytest twin tested the reconciler's body and left the runner's CALL to it uncovered, so removing the call kept the pytest half green while the shell half went red.
Prove by removal
Five mutations, each applied by exact string match with the file asserted to still parse:
False-positive budget first
A guard that reddens a legitimate write is worse than the mode it closes. The full corpus with a cluster is 249 passed, and none of the 12 write sites already in the corpus reddened.
Two of my own arms were wrong
Recorded because each produced a green that meant nothing.
An arm asserting only
failed=1passed before the feature existed. The inner test called a function not yet written, got anAttributeError, and the outer arm read that as the comparison failing. Naming the numbers is what makes the red the right red.A multi-word pattern can straddle pytest's word wrap.
expect.refusalanchors each pattern to oneEline, and pytest wraps a long traceback line — so matchingwrote no rowsfailed against a message that contained it, which reads exactly like "the guard did not fire". The refusal now leads with the mode's own kebab-case id, which is one token and cannot be split.Docs in the same commit
VACUITY_MODES.mdmoves the mode to section 2 and keeps a back-reference where it was. Its stated totals go 27 → 28 refused and 45 → 44 not refused — checked by the corpus's own arms, which is how I learned the numbers rather than deciding them.TESTS.mdgains section 22, and the section-18 paragraph that listed this gap is rewritten rather than deleted, because a reader who knew the gap needs to find out where it went.Gate
Based on
d0ac4f5d(current main).🤖 Generated with Claude Code
https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a