test(seed): pin reader_lock_acquire in hardlink_clone_into (#12)

Co-authored-by: Claude <claude@gitdan.com>
Co-committed-by: Claude <claude@gitdan.com>
This commit was merged in pull request #12.
This commit is contained in:
2026-08-24 23:05:18 +00:00
committed by claude
parent df6f1b91fb
commit 1aca90b461
2 changed files with 109 additions and 5 deletions
+19 -5
View File
@@ -452,7 +452,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 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 |
| `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, and the publisher's log showing it waited on the consumer's own reader-lock marker before reclaiming a rotated snapshot |
| `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. |
@@ -472,7 +472,7 @@ 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` uses the shape four times over, on two
unlink. `seed-target-dir-selftest.sh` uses the shape five 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
@@ -483,9 +483,14 @@ 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.
concurrency is incidental to the determinism. Scenario 11 reuses 8a's exact
stub and the same forced rotation, but reads a different witness: not the
consumer's own checks, but a line in the *publisher's* log reporting that it
waited on the reader marker this consumer's clone wrote — `reader_lock_acquire`
is exercised by 8a already, but nothing asserts it actually fired until this
scenario reads that line back. 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
@@ -517,6 +522,15 @@ 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.
Scenario 11 applies the same discipline to a witness outside the clone
entirely: not which of several checks inside `hardlink_clone_into` caught a
fault, but whether `reader_lock_acquire`'s marker was observed by anything
outside it at all. The consumer's own log and exit status are silent either
way — a run with the marker deleted still succeeds — so what is asserted is
one line in the *publisher's* log reporting that it waited. Deleting
`reader_lock_acquire` (issue #10) leaves that line unwritten without failing
anything else in the suite.
The action YAML holds no logic beyond wiring; everything testable lives in
`scripts/`. A composite action needs `shell: bash` on every `run:` step, and
the actions reach their shared scripts through