docs(readme): census the three concurrency-scenario shapes honestly

Three corrections to the methodology paragraph, all of the same kind: it
stated as fact things that hold for some scenarios and not others.

"Run the real scripts as real concurrent processes" is true of seed scenario
7 and half-true of 8a; 8b, prune 12 and publish 6-8 spawn nothing. A reader
who stopped at that topic sentence would take away "spawn real concurrent
processes", which is the instinct that produced #3. The paragraph now leads
with the three shapes actually in use and says which scenarios take each:
a genuine race whose invariant holds under any interleaving; a PATH stub that
places the interference inside the window; and a synthetic stand-in for the
other side where the artefact is itself the contract.

That third shape — publish-snapshot-selftest.sh's held .reading-* marker —
was unmentioned, so the README implied that suite stubs something it does
not. Its own defence (the marker IS the contract between the two sides, and
racing a real slow consumer would make the suite's runtime the thing under
test) is a good reason and now sits beside the other two.

"Where more than one guard could catch a fault, the scenario asserts which
one did" was written as description when it is a target: seed scenario 9 does
not, which is exactly why #5's mutation survives it. Stated as the rule plus
its one live exception — a rule asserted as fact with a known counterexample
is the same defect as the sentence this paragraph replaced.
This commit is contained in:
2026-08-24 12:04:35 -05:00
parent 5551e994da
commit 0ee21bfbc8
+33 -16
View File
@@ -370,26 +370,43 @@ Every suite runs the actual script, not a reimplementation of its logic, and
every fix scenario is paired with a control that reproduces the bug — a every fix scenario is paired with a control that reproduces the bug — a
scenario that passes either way proves nothing. scenario that passes either way proves nothing.
The concurrency scenarios run the real scripts as real concurrent processes, The concurrency scenarios take one of three shapes, and none of them races
but they never race for the interleaving. The interfering step is placed for the interleaving its assertion depends on.
inside the window by stubbing, on `PATH`, a command the code under test calls
at a known point: `prune-cache-selftest.sh` scenario 12 stubs `du`, so the **A genuine race whose asserted invariant holds under any interleaving.**
pass's own measurement publishes a reader marker strictly between its check `seed-target-dir-selftest.sh` scenario 7 starts two real seeds on one cache
and its unlink; `seed-target-dir-selftest.sh` scenarios 8a and 8b stub `cp`, key and asserts only what must be true whichever of them wins the rename.
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. **A `PATH` stub on a command the code under test calls at a known point**,
Placement is then a fact rather than a scheduling outcome — and each stub which places the interference inside the window rather than hoping it lands
asserts that it fired, because a scenario whose interference silently did not there. `prune-cache-selftest.sh` scenario 12 stubs `du`, so the pass's own
happen passes for the wrong reason. 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.
**A synthetic stand-in for the other side, where that artefact *is* the
contract.** `publish-snapshot-selftest.sh` scenarios 6 to 8 hold a
`.reading-*` marker instead of running a slow consumer: the marker is the
whole agreement between reader and publisher, so holding one is being a
reader, and racing a real one would make the suite's runtime the thing under
test.
Gating the interfering step on *observed progress* of the step it interferes Gating the interfering step on *observed progress* of the step it interferes
with was the earlier answer here, and it is not one: seeing that a walk has with was an earlier answer here, and it is not one: seeing that a walk has
started says nothing about where it will be when the interference lands, so started says nothing about where it will be when the interference lands, so
the assertion downstream held only some of the time (issue #3). the assertion downstream held only some of the time (issue #3). No scenario
does it any more.
Where more than one guard could catch a fault, the scenario asserts *which* Where more than one guard could catch a fault, a scenario should assert
one did — otherwise deleting the guard under test leaves the suite green *which* one did — otherwise deleting the guard under test leaves the suite
because a sibling fires in its place. 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).
The action YAML holds no logic beyond wiring; everything testable lives in The action YAML holds no logic beyond wiring; everything testable lives in
`scripts/`. A composite action needs `shell: bash` on every `run:` step, and `scripts/`. A composite action needs `shell: bash` on every `run:` step, and