test(seed): pin the two unguarded terms of the torn-clone condition #9

Merged
claude merged 3 commits from test/pin-clone-guards into main 2026-08-24 19:49:21 +00:00
3 Commits
Author SHA1 Message Date
claude 31b4113a26 docs(readme): drop an ordering claim scenario 9 does not make
The previous wording said the unshare pass aborts the clone "before the copy's
own exit status is ever consulted", which describes neither version. Unmutated,
the status is consulted immediately after the copy and fires first, so the
unshare pass is never reached; mutated, there is no check left to consult at
either point. As written a reader could take it for a claim that
unshare_mutable_paths runs before the torn-clone condition inside
hardlink_clone_into, which is the kind of ordering this file is otherwise
careful to state exactly (see the reader-marker ordering proof it sits under).

Names the mutation instead: deleting the exit-status check does not change the
outcome, because the unshare pass aborts the clone in its place. The mechanism
is unchanged and still holds — `cp -al` over a mode-000 source creates the
destination preserving mode 000 before failing, and `find | xargs` over that
returns 1 under pipefail, which _unshare_files propagates.
2026-08-24 14:28:08 -05:00
claude eb7878b822 docs(readme): correct the scenario census and the #5 citation
Review of #9 found the methodology section falsified by that PR and owned by
nobody — the scoping that fenced it off was wrong, #8 never touches these
paragraphs.

Three fixes:

* The PATH-stub paragraph named 8a and 8b as the seed suite's stubs. It is now
  four scenarios on two commands: 8a/8b/8c stub `cp` at the clone, 10 stubs it
  one level down at the per-file unshare, and 8d stubs `stat` — a mechanism
  the paragraph did not mention at all, and the only way to make an identity
  that could not be READ the sole witness.

* The assert-which-guard-fired paragraph cited issue #5 as a live example of a
  surviving mutation. #5 is the issue this PR closes, so a reader following
  that citation landed on "removing it leaves every suite green", which is no
  longer true. Scenario 9's own sentence stands — the matrix confirms it
  survives every mutant — so it now says WHY it survives (an unreadable source
  leaves the staging dir at mode 000, and the unshare pass aborts the clone
  before the copy's exit status is consulted) instead of citing a closed
  issue.

* Added the mutual-masking hazard the sweep turned up, since it is the general
  lesson rather than a fact about two particular terms: two guards that can
  each catch the same fault make each other unnecessary, so no fixture built
  around that fault pins either one.

Also names scenario 8d for what it is in its own comment — a regression guard
on a defensive term, not a reproduction of a reachable state. Every route to
the state it constructs is closed off (a rotation hands the witness to 8a, a
genuinely absent source hands it to 8c), which is the reason it is worth
pinning rather than a reason to doubt it.
2026-08-24 14:02:10 -05:00
claude bd60b430e0 test(seed): pin the two unguarded terms of the torn-clone condition
hardlink_clone_into's torn-clone detection is a four-term condition, and a
mutation sweep found two of the four unpinned: removing either
`[ "$cp_rc" -eq 0 ]` (issue #5) or `[ "$i_before" != missing ]` left all five
suites green. They were unpinned for the same reason — they mask each other.
A source that vanishes mid-clone reads as `missing` at both ends AND fails
`cp -al`, so with both terms present either one catches it and neither is
individually necessary.

Isolating them needs a state each term alone can see:

  8c  cp reports failure over a tree that is in fact whole. Neither inference
      sees anything — 0 entries short, one unchanged inode — so the exit
      status is the only witness. Forced with the PATH stub 8a/8b already
      use, on the consumer's own top-level clone.
  8d  both identity reads fail while the copy succeeds. `_dir_inode` folds
      every stat failure into the string `missing`, so two failed reads
      compare equal TO EACH OTHER; without the sentinel term the tree is
      published on the strength of two errors. Stubs `stat` narrowly — only
      the `%i` reads of this clone's own source — because taking the source
      away would fail `cp -al` too and pin 8c's property over again.

The sweep also found `_unshare_files`'s xargs status unpinned, which is the
guard that stops a staging tree whose dep-info files still point at the
SOURCE's inodes from being renamed into place — not a tear, so all four
clone checks pass it, and exactly the silent cross-branch stale-reuse the
scheme exists to prevent. Scenario 10 pins it by refusing the dep-info
unshares and asserting the clone discards rather than publishes.

assert_tear gains an `unreadable` identity expectation, and its `same` case
now demands a READ identity rather than two equal strings — `missing` equals
`missing`, which is the exact confusion 8d exists to pin.

Each of the five pinning scenarios was run in isolation against each
mutation; the result is a clean diagonal, so every scenario fails only for
its own term.

Closes #5
2026-08-24 13:24:53 -05:00