test: stage the upgrade fixtures unconditionally, so a leftover cannot win (#1090) - #1098
Conversation
linuxhikerpm
left a comment
There was a problem hiding this comment.
The staging change is the right property: if [ ! -f "$dst" ] lets a leftover install script win, and because that leftover was never in STAGED the EXIT trap never removed it either. Unconditional copy plus byte-for-byte restore of a pre-existing file is the correct ownership rule for a prefix this suite does not own. The two new arms (count of staged fixtures, content is the committed fixture) are the right pins; a vanished fixture would otherwise leave the content arm comparing nothing.
I am not approving this HEAD.
GitHub reports mergeable: CONFLICTING / DIRTY against current main (0d3100dd, v1.0-alpha4). The base is 93d9dc7. Since then main closed Unreleased into ## [1.0-alpha4] - 2026-09-17 (#1099 / #1100). The patch still inserts under the old ### Fixed that is now the shipped alpha4 section. A conflict-resolution that keeps that hunk would publish a post-tag test-harness fix as if it had shipped in alpha4.
Please rebase onto current main and put the CHANGELOG entry under a new ## [Unreleased] / ### Fixed. Do not fold it into the closed alpha4 notes.
Not merged.
jdatcmd
left a comment
There was a problem hiding this comment.
The fix is right and the arm is right. One defect in the restore path, reproduced, and two smaller notes. Not approving yet because the first one loses data that is currently evidence.
The restore path loses the original across a crashed run
cp -p "$dst" "$dst.pgcbak" runs unconditionally when $dst exists. If a prior run died before cleanup, $dst already holds the fixture and $dst.pgcbak already holds the original. The second run then overwrites the backup with the fixture.
Reproduced with your exact logic:
start: ext=[ORIGINAL-LEFTOVER]
run 1 crashes: ext=[COMMITTED-FIXTURE] bak=[ORIGINAL-LEFTOVER]
run 2 stages: ext=[COMMITTED-FIXTURE] bak=[COMMITTED-FIXTURE] <- original gone
after cleanup: ext=[COMMITTED-FIXTURE]
Net: the pre-existing file is permanently replaced by the fixture, silently. That is the outcome your comment says the preserve-rather-than-delete design exists to avoid, reached by a different route.
It is not hypothetical here. Those leftovers are the evidence for #901. PG15's alpha2 leftover is fead351f84ca, the v1.0-alpha2 TAG content, against a committed fixture that differed. One crashed run followed by one clean run would have destroyed that, and nothing would have said so.
The guard is one test:
[ -f "$dst" ] && [ ! -f "$dst.pgcbak" ] && cp -p "$dst" "$dst.pgcbak"I would go further and treat an existing .pgcbak as a named condition rather than a silent one, because its presence means a previous run died and the prefix is already mid-surgery. A check saying so costs nothing and turns an invisible state into a reported one.
Two smaller things
The version list is written twice, at lines 100 and 127, and the count is pinned to a literal 3 at a third place. Three copies of one fact. Adding a fixture means editing all three, and editing two of them leaves an arm that passes while testing less than it names. A single _NUC_FIXTURES used by the staging loop, the arm, and the count would make that impossible.
cp -p cannot preserve ownership when the suite is not root. The leftovers on my box are root-owned and the suite runs as postgres, so a restore gives back a postgres-owned file where a root-owned one was. Content is preserved, which is what matters, but the prefix is not restored exactly and the comment claims "byte for byte". Worth saying "content" rather than "byte for byte", or noting the limit.
What I verified and found sound
set -uo pipefail with no -e, so [ -f "$dst" ] && cp returning false cannot abort the run. That idiom is safe here and I checked rather than assumed.
The count arm is not redundant with the content arm: _nuc_staged counts only fixtures that exist, so a vanished fixture gives 2 against a pinned 3 and fails, where the content arm alone would compare nothing and report clean. That is the right pairing and it is the shape #1096 needed.
Your two-direction evidence is the strongest part of the PR. A differing leftover that still CONVERGED (11 passed + 0 failed, having tested a file nobody committed) is the half that would never have been noticed, and an invalid leftover as the discriminator is the right instrument: it cannot pass if read, so it distinguishes "read the leftover and converged anyway" from "read the fixture".
Sequencing
This conflicts with my #1103 on test/native_upgrade_converge.sh and CHANGELOG.md. Both edit the staging region. Land yours first: mine adds a fixture-vs-tag check that sits beside your staging block and I would rather rebase onto your shape than the reverse.
Worth knowing they are genuinely independent defects, and one run shows both: on my box alpha3's fixture-vs-tag check PASSES while its convergence arm FAILS. The fixture is correct and the suite is not reading it. Yours fixes the reading, mine fixes the fixture.
…901) Two defects in my own first version of this check, found reviewing it against the standard I had just applied to #1098. THE LIST WAS HARDCODED, AND COULD NOT SEE ITS OWN INCOMPLETENESS. `_FX_TAGGED` named three versions while the fixtures directory holds four. The one case this arm exists to catch is a NEW fixture captured wrongly at cycle-open, and a hardcoded list is exactly what omits a new fixture. It would have gone on passing while checking nothing about the thing it was added for. Derived from the fixtures on disk now, minus `1.0-alpha`, which is synthetic and documented as such. A fixture added next cycle is checked without anyone remembering this file exists. The sweep is a claim too, so it carries a premise. An empty derivation would make every arm below vanish and the suite would report clean having compared nothing. premise: the fixture sweep found fixtures to compare against their tags PASS THE LOCAL TAG IS TRUSTED AND THAT LIMIT WAS UNWRITTEN. `git fetch` never moves an existing local tag, so a stale one makes this arm compare against the wrong blob: it reports a drift that is not there, or misses one that is. This repository has already had a false release-integrity issue filed off a stale local ref. The limit is now stated where the arm is, with the command that settles it: git ls-remote origin refs/tags/v<version> Checked today, and they agree: local v1.0-alpha2 and the server both at 7c317d3. That is why this is a documented limit rather than a bug. 14 passed + 1 failed = 15, the failure being #1090's leftover on this box Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
…t win (commandprompt#1090) native_upgrade_converge creates an old-version extension to upgrade from, which needs the old base install script in the extension directory. It staged each fixture only `if [ -f "$src" ] && [ ! -f "$dst" ]`. This repository shipped pgcolumnar--1.0-alpha2.sql and pgcolumnar--1.0-alpha3.sql from the tree until the cycle-open rename moved them under test/fixtures, so any prefix installed before that still carries them and nothing prunes them. Because the leftover was not staged, the EXIT trap did not remove it either: it persisted and won again on every later run. IT FAILED IN BOTH DIRECTIONS. Measured on one machine, same commit, two prefixes: leftover DIFFERS PG15 holds the v1.0-alpha2 TAG content (fead351f84ca) against the fixture's cbb4f36e4308. The suite reported 11 passed + 0 failed -- CONVERGED, having tested a file nobody committed, with nothing in its output naming which file it read. leftover BROKEN a one-line invalid file gave 8 passed + 3 failed: a red for a defect that is not in the tree. The second is how this was noticed. The first is what it costs, and it is why the fix is not simply "delete the leftovers". That the suite reads the leftover rather than the fixture was established with a discriminator that cannot be ambiguous: a syntactically invalid leftover must fail if it is read and pass if it is not. It failed. Staging is now unconditional. A pre-existing file is preserved and restored byte for byte rather than deleted -- this suite did not create it and must not change the state of a prefix it does not own. Verified: the planted leftover is still 8e3f1a0f2317 after the run, and PG15's is still fead351f84ca. Two arms say the property out loud, so a future change that reintroduces a conditional cannot pass silently. The count is pinned separately from the content, because without it a fixture that vanished would leave the content arm comparing nothing and reporting clean. Removal proof -- restore the conditional, keep the arms: every staged install script is the committed fixture, not a leftover: got [1.0-alpha2] want [] Verified: 13/13 on pg18a (no leftovers) and pg19a (leftovers present), 967 + 0 in harness_selftest, shellcheck clean. The ledger is untouched: this suite is registered but uncovered, so no rows move and the gate is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
8f23ccc to
280d5af
Compare
…mpt#901) `native_upgrade_converge` claims every released starting point upgrades to the current catalog. It can only claim that if each fixture IS the released starting point, and `1.0-alpha2`'s was not. The fixtures are made by renaming the root base script at cycle-open. That equals the release only if nothing touched the file between the tag and the rename. For alpha2 two post-release `set_options` fixes did, so the fixture was the released script plus 50 lines it never shipped with: ERRCODE = 'wrong_object_type' 42809 rather than plpgsql's default P0001 a relation that is not columnar refused Taken from the tag now, and the corrected fixture CONVERGES, so the wrong one was not hiding a broken upgrade. It was hiding that nothing tested the real one. A CHECK, COMPARING BLOB IDS RATHER THAN DIFFING. A blob id is a lookup: no similarity heuristic and no pathspec can distort it, and a rename-detection argument once fabricated an `R098` against this very file. the 1.0-alpha2 fixture is byte-identical to what v1.0-alpha2 shipped PASS the 1.0-alpha3 fixture is byte-identical to what v1.0-alpha3 shipped PASS the 1.0-alpha4 fixture is byte-identical to what v1.0-alpha4 shipped PASS Removal proof, the pre-fix fixture against the tag: FAIL got [34a359d] want [ef3f381] It SKIPS where tags are absent, with the reason named. `actions/checkout` takes one ref at depth 1, so this cannot run in CI. It runs locally and in the five-major release gate, which is where a fixture is captured and therefore where it can be captured wrongly. `1.0-alpha` IS EXCLUDED, AND THE ISSUE'S FIX FOR IT DOES NOT WORK. commandprompt#901 proposed replacing that fixture with the real `1.0-dev` script from `v1.0-alpha`. Tried it: ERROR: could not find function "columnar_handler" in file "pgcolumnar.so" `v1.0-alpha` shipped `pgcolumnar--1.0-dev.sql` with `default_version = 1.0-dev`, and no tag ships a `pgcolumnar--1.0-alpha.sql` at all. The real script names the pre-rename C symbol, and that rename is exactly why `ALTER EXTENSION UPDATE` is mandatory rather than cosmetic. So no single-library test can start from a genuine `1.0-dev` or `1.0-alpha` install. The arm is kept rather than dropped: it is the only cover for the `1.0-dev--1.0-alpha` and `1.0-alpha--1.0-alpha2` upgrade scripts, which ship. The header now says it tests catalog shape rather than a released artifact, and the exclusion is written down rather than left as a silent gap. NOT FIXED HERE: the suite still stages a fixture only when nothing is already installed, so a leftover in the extension directory wins over the committed file (commandprompt#1090, @OffgridwithJD's commandprompt#1098). The two are independent and this run shows both: alpha3's fixture-vs-tag check PASSES while its convergence arm FAILS, because the fixture is right and the suite is not reading it. 13 passed + 1 failed = 14, the failure being commandprompt#1090 on this box Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
|
Rebased onto The
|
jdatcmd
left a comment
There was a problem hiding this comment.
Approving. All three points addressed, and I verified the fix against my own reproduction rather than reading the diff.
The restore path
Ran the fixed logic on the exact entry state I reported:
start: ext=[ORIGINAL-LEFTOVER]
run 1, crashed: ext=[FIXTURE] bak=[ORIGINAL-LEFTOVER]
run 2 staged: ext=[FIXTURE] bak=[ORIGINAL-LEFTOVER] <- backup not overwritten
run 2 cleanup: ext=[ORIGINAL-LEFTOVER]
VERDICT: ORIGINAL SURVIVED
Your removal proof carries the unguarded control beside it, which is the half that makes the guarded arm mean something.
The named premise for a pre-existing .pgcbak is better than what I suggested. I proposed guarding the copy; you guarded it and made the mid-surgery state a reported condition with instructions for clearing it. A prefix left half-staged by a dead run is now a thing the suite says out loud rather than a thing it silently works around.
The other two
byte for byte is gone, and the replacement is more useful than a correction: it says CONTENT is restored, names cp -p as unable to give back an owner the suite does not have, and records that these leftovers are root-owned. That is the limit stated where someone would hit it.
Four copies of the version list, not the three I counted. I missed the one in the convergence loop. One _NUC_FIXTURES now, and the single remaining literal is the array definition, which is the right place for it.
What I checked independently
Both halves of native_upgrade_converge.sh survived the conflict with #1103 intact: my derived fixture-vs-tag sweep and your staging block sit in sequence, neither reduced. 18 passed + 0 failed on the merged tree.
Dropping your #1104 branch in favour of mine was the right call on the merits and I would have said so if it were reversed. Your two measurements on #1105 both landed: fetch-depth: 2 is sufficient including the tree read, which I had verified only for the commit, and the trigger is a tag push rather than a schedule, which my commit message now says.
The tautology you found is the better argument for that change than anything in my diff. auto resolving to origin/main when origin/main IS HEAD means the ceiling was compared against the commit it was read from on every nightly that has ever passed. I have put it in the code comment where it belongs.
…safe (commandprompt#996) Entries insert as the first child of one heading, so two PRs sharing no file but CHANGELOG.md still conflict. Nine of 27 merges in one day touched it; three merge commits that day exist only to resolve it. Measured on the real pair, commandprompt#1098 and commandprompt#1106, both opening a new [Unreleased]: default 3-way rc=1, 2 conflict markers merge=union rc=0, 0 markers, ONE [Unreleased], both entries intact Union emits identical lines once, which is why the headings are not doubled: 7640 + 57 + 52 = 7749 against an actual 7744. THE DRIVER DOES NOT SHIP ALONE. Union keeps both sides of a divergent hunk silently, and a release cut EDITS the line pending PRs append beneath. Reproduced: a PR merged into a release cut files its entry inside the section that just shipped, rc=0, no marker. Today that conflicts and a human sees it. So docs_style.sh gains the check that catches it: each dated section holds the entries its own tag shipped, and no more. Proved in three directions -- an entry added to closed alpha4 gives got [113] want [112]; one removed from alpha2 gives got [57] want [58]; removing the driver line reddens its own arm. v1.0-alpha3 HAS NO BASELINE: that tag shipped with its section still named [Unreleased], the release having been dated after it was tagged. That is ASSERTED rather than skipped -- "v1.0-alpha3 shipped no dated section of its own, so it has no baseline" -- so "this cannot be compared" is itself checked, and the arm reddens if a release ever does carry one (measured: got [1] want [0]). False-positive budget measured before shipping: 1 of 4 released sections, and that one is this. REACHING FOR A SKIP THERE WAS THE WRONG INSTRUMENT, and two guards said so. This suite keeps its own tally and emits none of the machine RESULT vocabulary, so lib.sh's check_skip is `command not found` inside it -- printing nothing, counting nothing, failing nothing, while the suite reports PASSED. Adding a local one then tripped selftest 400, which refuses `echo "SKIP` in any file that calls check, because a SKIP is an outcome a count and a record must see and this suite has neither. A property it cannot compare is now a note(): printed, counted as nothing, claiming no outcome. awk kept portable: no gawk-only three-argument match(); identical output under mawk, gawk and this box's default awk. bash -n and shellcheck -S error clean. docs_style.sh holds no ledger rows, so no rows move. Verified: docs_style.sh 35 checks rc=0 stderr empty; harness_selftest 976 checks 0 failed, including selftest 400's own SKIP guard; pytest guards 346 passed, 897 checks, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
…safe (commandprompt#996) Entries insert as the first child of one heading, so two PRs sharing no file but CHANGELOG.md still conflict. Nine of 27 merges in one day touched it; three merge commits that day exist only to resolve it. Measured on the real pair, commandprompt#1098 and commandprompt#1106, both opening a new [Unreleased]: default 3-way rc=1, 2 conflict markers merge=union rc=0, 0 markers, ONE [Unreleased], both entries intact Union emits identical lines once, which is why the headings are not doubled: 7640 + 57 + 52 = 7749 against an actual 7744. THE DRIVER DOES NOT SHIP ALONE. Union keeps both sides of a divergent hunk silently, and a release cut EDITS the line pending PRs append beneath. Reproduced: a PR merged into a release cut files its entry inside the section that just shipped, rc=0, no marker. Today that conflicts and a human sees it. So docs_style.sh gains the check that catches it: each dated section holds the entries its own tag shipped, and no more. Proved in three directions -- an entry added to closed alpha4 gives got [113] want [112]; one removed from alpha2 gives got [57] want [58]; removing the driver line reddens its own arm. v1.0-alpha3 HAS NO BASELINE: that tag shipped with its section still named [Unreleased], the release having been dated after it was tagged. That is ASSERTED rather than skipped -- "v1.0-alpha3 shipped no dated section of its own, so it has no baseline" -- so "this cannot be compared" is itself checked, and the arm reddens if a release ever does carry one (measured: got [1] want [0]). False-positive budget measured before shipping: 1 of 4 released sections, and that one is this. REACHING FOR A SKIP THERE WAS THE WRONG INSTRUMENT, and two guards said so. This suite keeps its own tally and emits none of the machine RESULT vocabulary, so lib.sh's check_skip is `command not found` inside it -- printing nothing, counting nothing, failing nothing, while the suite reports PASSED. Adding a local one then tripped selftest 400, which refuses `echo "SKIP` in any file that calls check, because a SKIP is an outcome a count and a record must see and this suite has neither. A property it cannot compare is now a note(): printed, counted as nothing, claiming no outcome. awk kept portable: no gawk-only three-argument match(); identical output under mawk, gawk and this box's default awk. bash -n and shellcheck -S error clean. docs_style.sh holds no ledger rows, so no rows move. Verified: docs_style.sh 35 checks rc=0 stderr empty; harness_selftest 976 checks 0 failed, including selftest 400's own SKIP guard; pytest guards 346 passed, 897 checks, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
…safe (commandprompt#996) Entries insert as the first child of one heading, so two PRs sharing no file but CHANGELOG.md still conflict. Nine of 27 merges in one day touched it; three merge commits that day exist only to resolve it. Measured on the real pair, commandprompt#1098 and commandprompt#1106, both opening a new [Unreleased]: default 3-way rc=1, 2 conflict markers merge=union rc=0, 0 markers, ONE [Unreleased], both entries intact Union emits identical lines once, which is why the headings are not doubled: 7640 + 57 + 52 = 7749 against an actual 7744. THE DRIVER DOES NOT SHIP ALONE. Union keeps both sides of a divergent hunk silently, and a release cut EDITS the line pending PRs append beneath. Reproduced: a PR merged into a release cut files its entry inside the section that just shipped, rc=0, no marker. Today that conflicts and a human sees it. So docs_style.sh gains the check: each dated section holds the entries its own tag shipped, and nothing else. THE KEY IS THE ENTRY, not a count -- a count would let one post-tag entry be swapped for another with the arm still green. IT FOUND ONE ALREADY ON MAIN. ## [1.0-alpha3] carries an entry v1.0-alpha3 never shipped, added by d978e7f (commandprompt#432) on 2026-09-09, seven days after the 2026-09-02 tag. Found by jdatcmd in review. It cannot be corrected without making a second section wrong -- the work shipped in the alpha4 cycle and the v1.0-alpha4 tag does not carry it either -- so a released section MAY diverge, but only by being recorded in test/changelog_post_tag.txt with a reason visible in the diff. Two arms guard that file: every row needs a reason, and no row may be stale. Seven mutations, each restored byte-exact: an entry added to closed alpha4, one removed from alpha2, the allowance row deleted, the allowance naming a different entry, the reason blanked, a stale allowance row, and the driver line removed. Each reddens its own arm. Clean tree: 42 checks, rc=0. THE FIRST VERSION OF THIS CHECK WAS MEASURED AGAINST A STALE TAG. This tree's local v1.0-alpha3 was d9df031 against the server's cec9e9b, and git fetch never moves a tag that already exists. That produced a false narrative (alpha3 "shipped with its section still named [Unreleased]"), a skip for it, and a "false-positive budget of 1 of 4" -- all three wrong, with the arm green where the tree was in violation. jdatcmd caught it by checking their own refs against git ls-remote first. The suite now tells a reader to verify a tag before believing a red arm. Reaching for a skip was also wrong, and two guards said so. This suite keeps its own tally and emits none of the machine RESULT vocabulary, so lib.sh's check_skip is `command not found` inside it. A local one then tripped selftest 400, which refuses `echo "SKIP` in any file that calls check. A property this suite cannot compare is now a note(): printed, counted as nothing, claiming no outcome. Selftest 070 caught a third: introducing `comm` makes every `| sort` in the file collation-sensitive, and comm compares byte-wise without checking its inputs agree. All nine sorts are pinned LC_ALL=C, including the five that predate this block, and so is comm. KNOWN LIMIT: at depth 1 with no tags every section arm is skipped, so CI cannot run any of this. It runs locally and in the five-major release gate. Verified: docs_style 42 checks rc=0; harness_selftest 976 checks 0 failed, including selftest 070 and 400's own arms; pytest guards 346 passed, 897 checks, 0 failed. docs_style.sh holds no ledger rows, so no rows move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
…safe (commandprompt#996) Entries insert as the first child of one heading, so two PRs sharing no file but CHANGELOG.md still conflict. Nine of 27 merges in one day touched it; three merge commits that day exist only to resolve it. Measured on the real pair, commandprompt#1098 and commandprompt#1106, both opening a new [Unreleased]: default 3-way rc=1, 2 conflict markers merge=union rc=0, 0 markers, ONE [Unreleased], both entries intact Union emits identical lines once, which is why the headings are not doubled: 7640 + 57 + 52 = 7749 against an actual 7744. THE DRIVER DOES NOT SHIP ALONE. Union keeps both sides of a divergent hunk silently, and a release cut EDITS the line pending PRs append beneath. Reproduced: a PR merged into a release cut files its entry inside the section that just shipped, rc=0, no marker. Today that conflicts and a human sees it. So docs_style.sh gains the check: each dated section holds the entries its own tag shipped, and nothing else. THE KEY IS THE ENTRY, not a count -- a count would let one post-tag entry be swapped for another with the arm still green. IT FOUND ONE ALREADY ON MAIN. ## [1.0-alpha3] carries an entry v1.0-alpha3 never shipped, added by d978e7f (commandprompt#432) on 2026-09-09, seven days after the 2026-09-02 tag. Found by jdatcmd in review. It cannot be corrected without making a second section wrong -- the work shipped in the alpha4 cycle and the v1.0-alpha4 tag does not carry it either -- so a released section MAY diverge, but only by being recorded in test/changelog_post_tag.txt with a reason visible in the diff. Two arms guard that file: every row needs a reason, and no row may be stale. Seven mutations, each restored byte-exact: an entry added to closed alpha4, one removed from alpha2, the allowance row deleted, the allowance naming a different entry, the reason blanked, a stale allowance row, and the driver line removed. Each reddens its own arm. Clean tree: 42 checks, rc=0. THE FIRST VERSION OF THIS CHECK WAS MEASURED AGAINST A STALE TAG. This tree's local v1.0-alpha3 was d9df031 against the server's cec9e9b, and git fetch never moves a tag that already exists. That produced a false narrative (alpha3 "shipped with its section still named [Unreleased]"), a skip for it, and a "false-positive budget of 1 of 4" -- all three wrong, with the arm green where the tree was in violation. jdatcmd caught it by checking their own refs against git ls-remote first. The suite now tells a reader to verify a tag before believing a red arm. Reaching for a skip was also wrong, and two guards said so. This suite keeps its own tally and emits none of the machine RESULT vocabulary, so lib.sh's check_skip is `command not found` inside it. A local one then tripped selftest 400, which refuses `echo "SKIP` in any file that calls check. A property this suite cannot compare is now a note(): printed, counted as nothing, claiming no outcome. Selftest 070 caught a third: introducing `comm` makes every `| sort` in the file collation-sensitive, and comm compares byte-wise without checking its inputs agree. All nine sorts are pinned LC_ALL=C, including the five that predate this block, and so is comm. KNOWN LIMIT: at depth 1 with no tags every section arm is skipped, so CI cannot run any of this. It runs locally and in the five-major release gate. Verified: docs_style 42 checks rc=0; harness_selftest 976 checks 0 failed, including selftest 070 and 400's own arms; pytest guards 346 passed, 897 checks, 0 failed. docs_style.sh holds no ledger rows, so no rows move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
Fixes #1090.
native_upgrade_convergecreates an old-version extension to upgrade from, which needs the old base install script present in the extension directory. It staged each fixture only:This repository shipped
pgcolumnar--1.0-alpha2.sqlandpgcolumnar--1.0-alpha3.sqlfrom the tree until the cycle-open rename moved them undertest/fixtures, so any prefix installed before that still carries them and nothing prunes them. Because the leftover was never staged, the EXIT trap did not remove it either — it persisted and won again on every later run.It fails in both directions, and the quiet one is the one that matters
Measured on one machine, same commit, two prefixes:
fead351f84ca) against the fixture'scbb4f36e4308. The suite reported 11 passed + 0 failed — CONVERGED, having tested a file nobody committed, with nothing in the output naming which file it read.The second is how this was noticed. The first is what it costs, and it is why the fix is not simply "delete the leftovers".
That the suite reads the leftover was proven, not inferred
A leftover that merely differs cannot distinguish the two possibilities — it might be the fixture being read and converging anyway. So the discriminator was a syntactically invalid leftover, which must fail if it is read and pass if it is not:
It is read.
The fix
Staging is unconditional. A pre-existing file is preserved and restored byte for byte rather than deleted — this suite did not create it and must not change the state of a prefix it does not own.
Two arms state the property so a future change that reintroduces a conditional cannot pass silently. The count is pinned separately from the content, because without it a fixture that vanished would leave the content arm comparing nothing and reporting clean.
Removal proof
Restore the conditional, keep the arms:
It names the script that was wrong rather than only failing.
Verified
The ledger is untouched. This suite is registered but uncovered — 0 rows in main — so no rows move,
suites_not_coveredis unchanged, and the gate is unaffected. It also has no pytest twin and is in neitherCOMPLETEnorINCOMPLETE, so the two new checks carry no parity obligation.Why this is release-relevant
The five-major matrix is the release gate, and it runs on a long-lived box where these leftovers accumulate. Last night's run had this suite red on PG15, 16, 17 and 19, passing only on PG18 — the one prefix whose extension directory had been rewritten. Whoever runs the gate at the tag hits it.