From aee337a1cb0d01aec068478314623b65542ce642 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 17:57:27 -0500 Subject: [PATCH] docs(readme): catalogue scenario 11 and close its scratch self-check The suite-coverage table and the concurrency-scenario prose enumerate every scenario by number and by the property it pins, so adding one without listing it leaves the catalogue claiming complete coverage it no longer has. PR #9 updated the same table for the same reason when it added 8b/8c/8d/10; this follows it. Also adds the closing no-scratch-left-behind check that 8a and ALIAS end with. Scenario 11 is last in the file, so nothing downstream would have caught a leaked staging path or reader marker. --- README.md | 24 +++++++++++++++++++----- scripts/seed-target-dir-selftest.sh | 3 +++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f36e244..467d950 100644 --- a/README.md +++ b/README.md @@ -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 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. | @@ -442,7 +442,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 @@ -453,9 +453,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 @@ -487,6 +492,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 diff --git a/scripts/seed-target-dir-selftest.sh b/scripts/seed-target-dir-selftest.sh index 2411ed3..252bb15 100755 --- a/scripts/seed-target-dir-selftest.sh +++ b/scripts/seed-target-dir-selftest.sh @@ -720,6 +720,9 @@ ok "both sides of the race completed" grep -q "readers: waiting for 1 in-flight clone(s) of snapshot-${INTLK_BASE}" "$scratch/logPubIntlk" \ || { tail -40 "$scratch/logPubIntlk"; fail "the publisher never reported waiting on the consumer's reader marker — the interlock did not observably engage"; } ok "the publisher observably waited on the consumer's own reader marker before reclaiming the rotated snapshot generation" +leftovers=$(find "$root" -maxdepth 1 \( -name '.stage-*' -o -name '.reading-*' -o -name '.publish-*' \) -print) +[ -z "$leftovers" ] || fail "scratch left behind: ${leftovers}" +ok "no staging, reader-marker or deferred-generation scratch left behind" rm -f "$scratch/bin/cp" echo