Merge pull request 'test(seed): pin the two unguarded terms of the torn-clone condition' (#9) from test/pin-clone-guards into main

This commit was merged in pull request #9.
This commit is contained in:
2026-08-24 19:49:20 +00:00
2 changed files with 267 additions and 22 deletions
+29 -11
View File
@@ -422,7 +422,7 @@ bash scripts/selftest.sh --fast # fixture-only suites, no compiler
| suite | covers |
|---|---|
| `hardlink-clone-selftest.sh` | that a build in a clone cannot mutate its source — with a control proving a raw `cp -al` does. Needs a real compiler. |
| `seed-target-dir-selftest.sh` | seed-source preference, lock-file stripping, two jobs racing on one cache key, **and a seed whose source is rotated — or silently loses a subtree — underneath its clone**: the two ways a hardlink clone tears |
| `seed-target-dir-selftest.sh` | seed-source preference, lock-file stripping, two jobs racing on one cache key, **and one scenario per check a hardlink clone is validated against**: a source rotated wholesale, a subtree silently lost from the walk, a copy that reports failure over a tree both other checks read as whole, and a source identity that resolved at neither end — plus a staging tree that could not be privately owned being discarded rather than published |
| `publish-snapshot-selftest.sh` | the atomic swap, that a live consumer survives a republish, and the publisher's side of the rotation race: deferred reclamation under a live reader, and its sweep once the reader is gone |
| `prune-cache-selftest.sh` | liveness, protection, locking, eviction order, self-clear, **and that a cache a job claims *inside* the check-to-unlink window survives it** — against a real scratch `origin` |
| `restore-mtimes-selftest.sh` | the merge hazard and the watermark that closes it, including the two-jobs-one-namespace case. Needs a real compiler. |
@@ -442,13 +442,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
@@ -465,9 +472,20 @@ 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 deleting
the exit-status check does not change the outcome — the unshare pass aborts
the clone instead, 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