test(seed): pin the two unguarded terms of the torn-clone condition #9

Merged
claude merged 3 commits from test/pin-clone-guards into main 2026-08-24 19:49:21 +00:00
2 changed files with 39 additions and 11 deletions
Showing only changes of commit eb7878b822 - Show all commits
+27 -10
View File
@@ -381,13 +381,20 @@ key and asserts only what must be true whichever of them wins the rename.
which places the interference inside the window rather than hoping it lands
there. `prune-cache-selftest.sh` scenario 12 stubs `du`, so the pass's own
measurement publishes a reader marker strictly between its check and its
unlink; `seed-target-dir-selftest.sh` scenarios 8a and 8b stub `cp`, so the
consumer's own clone is what rotates the snapshot underneath it, or what
loses a subtree of its own source, strictly inside the identity window. 8a
does start a second real process — the actual `publish-snapshot.sh` — but the
stub is what fixes where its swap lands; the concurrency is incidental to the
determinism. Every stub asserts that it fired, because a scenario whose
interference silently did not happen passes for the wrong reason.
unlink. `seed-target-dir-selftest.sh` uses the shape four times over, on two
different commands. Scenarios 8a, 8b and 8c stub `cp`, so the consumer's own
clone is what rotates the snapshot underneath it, loses a subtree of its own
source, or reports a failure over a tree that is in fact whole — each strictly
inside the window that clone's checks cover; scenario 10 stubs `cp` one level
down instead, refusing the per-file copies that unshare the mutable paths.
Scenario 8d stubs `stat`, because the check it pins fires on an identity that
could not be READ rather than on one that changed, and the only way to make
that the sole witness is to fail the identity reads while the copy between
them succeeds. 8a also starts a second real process — the actual
`publish-snapshot.sh` — but the stub is what fixes where its swap lands; the
concurrency is incidental to the determinism. Every stub asserts that it
fired, because a scenario whose interference silently did not happen passes
for the wrong reason.
**A synthetic stand-in for the other side, where that artefact *is* the
contract.** `publish-snapshot-selftest.sh` scenarios 6 to 8 hold a
@@ -404,9 +411,19 @@ does it any more.
Where more than one guard could catch a fault, a scenario should assert
*which* one did — otherwise deleting the guard under test leaves the suite
green because a sibling fires in its place. Scenarios 8a and 8b of the seed
suite do; scenario 9 of the same suite does not yet, which is why a mutation
survives it (issue #5).
green because a sibling fires in its place. Scenarios 8a to 8d and 10 of the
seed suite do, and each is reddened by exactly one mutation of the clone's
checks. Scenario 9 does not, and a mutation still survives it: with its source
unreadable, `cp -al` leaves the staging directory at mode `000`, so the
unshare pass aborts the clone before the copy's own exit status is ever
consulted, and the assertion is satisfied down a path it was not written for.
That is the standing hazard here, and it is not hypothetical. Two guards that
can each catch the same fault mask each other, so **neither** is individually
necessary and no fixture built around that fault can pin either one — which is
how both `[ "$cp_rc" -eq 0 ]` and `[ "$i_before" != missing ]` sat unpinned
(issue #5) while looking well covered. Isolating a check means constructing
the state only it can see, not the state that trips several at once.
The action YAML holds no logic beyond wiring; everything testable lives in
`scripts/`. A composite action needs `shell: bash` on every `run:` step, and
+12 -1
View File
@@ -52,7 +52,9 @@
# identity read is reported as the string `missing`, so two of them
# compare equal to each other; without the term that rejects the
# sentinel, a clone whose source could not be identified at either end
# is published on the strength of two errors.
# is published on the strength of two errors. A regression guard on a
# defensive term rather than a reproduction of a reachable state — see
# the scenario's own comment.
# All four scenarios force their interleaving rather than racing for it,
# and each asserts WHICH check caught the tear, so none stays green if
# the check it exercises is removed.
@@ -486,6 +488,15 @@ echo "=== 8d: an identity that could not be read at either end ==="
# would differ from `missing` and the identity COMPARISON would become the
# witness instead — 8a's property, not this one. The assertion that the stub
# fired is therefore a count rather than a flag.
#
# So be clear about what this scenario is. It is NOT a reproduction of a state
# a CI job reaches: every route to it is closed off — a rotation hands the
# witness to 8a, and a source that is genuinely gone fails `cp -al` and hands
# it to 8c. It is a REGRESSION GUARD ON A DEFENSIVE TERM, and the thing it
# defends against is a sentinel comparing equal to itself, which is a property
# of the code rather than of the filesystem. That is worth pinning precisely
# because nothing else can reach it: a term no fixture exercises is the one a
# refactor drops without argument.
IDENT=$(cache_key feat/identity-unreadable)
IDENT_BASE=$(cache_key release/3)
make_wide_tree "$root/snapshot-$IDENT_BASE" identgen 4