test(seed): force scenario 8's interleaving instead of racing for it #4

Merged
claude merged 3 commits from fix/seed-selftest-flake into main 2026-08-24 17:29:49 +00:00
3 Commits
Author SHA1 Message Date
claude 0ee21bfbc8 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.
2026-08-24 12:04:35 -05:00
claude 5551e994da docs(readme): describe how the concurrency scenarios actually work now
README's Development section stated the repo's methodology for writing
concurrency scenarios as "gate the interfering step on observed progress of
the step it interferes with, so the window is hit deterministically". That
described the progress poll scenario 8 used, which this branch removes — and
the property it claims is precisely what issue #3 records as false: observing
that a walk has started says nothing about where it will be when the
interference lands.

Left standing it would tell the next contributor to build the next scenario
the way this one had to be rewritten. Replaced with what the suites do: stub,
on PATH, a command the code under test calls at a known point, so placement is
a fact rather than a scheduling outcome; assert the stub fired; and assert
which guard caught the fault where more than one could.

Also: the seed suite's table row now names both tear modes, and
publish-snapshot-selftest.sh's cross-reference points at 8a and 8b rather than
a scenario 8 that no longer exists. (publish-snapshot.sh's similar mislabel
predates this branch and is left alone.)

The stub directory and the real-cp lookup move up next to seed_with_stub, so
8b no longer depends on setup buried in 8a's block and either scenario can be
run or mutated alone.
2026-08-24 11:47:34 -05:00
claude dc0fef71f8 test(seed): force scenario 8's interleaving instead of racing for it
Scenario 8 started a real publisher once a consumer's clone was observed
past a fraction of the tree, then asserted the consumer ended up holding
the NEW generation. That premise is racy: when the publish lands after the
consumer's last identity read, the consumer legitimately completes a whole,
consistent generation 1 — and the assertion called that "the seeded tree is
truncated". The one gate this repo has was failing intermittently in the
most alarming direction available.

Replaced with two scenarios that place the interference deterministically,
the way prune-cache-selftest.sh scenario 12 places a marker inside the
check-to-unlink window: the consumer's own `cp` is stubbed, so whatever the
stub does happens strictly after hardlink_clone_into read the source's
entry count and inode and strictly before it reads that inode again.

  8a  the stub runs the real copy, then the real publish-snapshot.sh
      concurrently, and returns only once the swap is on disk. The copy
      succeeds and the staging tree is whole, so the source's identity is
      the only witness.
  8b  the stub renames a subtree out of the source's listing before the
      walk starts and back before the retry. The copy exits 0 and the
      source's identity never changes, so the entry count is the only
      witness — the silent truncation this whole guard exists for.

Each asserts WHICH of the clone's checks reported the tear (assert_tear),
so neither stays green if the check it exercises is deleted and another
happens to fire in its place.

Closes #3
2026-08-24 10:56:42 -05:00