diff --git a/README.md b/README.md index 132b226..45d826b 100644 --- a/README.md +++ b/README.md @@ -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 scenario that passes either way proves nothing. -The concurrency scenarios run the real scripts as real concurrent processes, -but they never race for the interleaving. The interfering step is placed -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 -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. -Placement is then a fact rather than a scheduling outcome — and each stub -asserts that it fired, because a scenario whose interference silently did not -happen passes for the wrong reason. +The concurrency scenarios take one of three shapes, and none of them races +for the interleaving its assertion depends on. + +**A genuine race whose asserted invariant holds under any interleaving.** +`seed-target-dir-selftest.sh` scenario 7 starts two real seeds on one cache +key and asserts only what must be true whichever of them wins the rename. + +**A `PATH` stub on a command the code under test calls at a known point**, +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. + +**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 -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 -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* -one did — otherwise deleting the guard under test leaves the suite green -because a sibling fires in its place. +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). The action YAML holds no logic beyond wiring; everything testable lives in `scripts/`. A composite action needs `shell: bash` on every `run:` step, and