# Row 188: stack-release tests for a carried proof pair and a proof file inside the repository Brief: `docs/plans/2026-10-20_proof-test-gaps.md` (on origin/refactor 525990ecb). Gitea #1686, P19 409 (M8) and 410 (M9), Filbert's N1 and N2 on row 187 (#1685 comment 29424). Owner Darkwing, reviewer Filbert, gate owner Sage. Mos ruled this row lands before the Part B window runs (mos-0956). ## Files | File | What | |---|---| | `build.patch` | the change, `git diff 525990ecb` over the one path below | | `candidate-manifest.sha256` | one line for that path | - Base: 525990ecb. The brief names 742e98976, and the three commits above it touch only docs and the queue. - Changed: `packages/cli/tests/stack-release.test.mjs` only. `scripts/stack-release.sh` is untouched, and its blob is still `2586a2a37cd2e7c7aeaba6d31490e5b7920fb4b2`, the one the Part B window file names. `scripts/webui-service.sh`, `docs/TOOLS.md` and `packages/webui` are untouched too. P19 407 and 408 are not in this row. Script line numbers below are at 525990ecb, which match the brief's (same blob). ## The tests One new top-level test, `T54 row 188`, after T53, with six subtests. Each one makes its own stand-in with `mk` and the preview flags with `seedPreview`, and ends with `noLeak`. That check fails on any open or read of a watched proof file, and on SENTINEL in any output, unit, pointer, log or steps log. | Subtest | Brief test | Setup | What it pins | |---|---|---|---| | test 1, chmod 644 | 1 | activate B with the pair, then chmod the file 644 | `--plan` for `activate C1` lists `proof: carried` and exactly one refusal, `proof-file-invalid: ... has mode 644, not 600 or 400`; the run refuses the same before anything stops; the pointer stays on B with the pair | | test 1, symlink | 1 | same, then the path becomes a symlink to a watched mode-600 file | the same, with `... is a symlink` | | test 1, removal | 1 | same, then the path is removed | the same, with `... cannot be found` | | test 2 | 2 | activate C1 with the pair, then chmod 644 | `--plan` for `rollback` (to B, whose Console takes the pair) lists `proof: carried` and the one refusal; the run refuses before anything stops; the pointer stays on C1 with the pair | | test 3 | 3 | a mode-600 file of the test's uid at `/proof-r188/proof.188`, with the pair on `activate C1` | the plan and the run refuse `... is inside the repository or the copies` | | test 4 | 4 | `/repo-link.188`, a directory symlink to `/proof-r188`, with the file given through it | the test asserts that the given path's last part is not a symlink and the path is outside REPO; the plan and the run refuse on its realpath with the same reason | "The run refuses before anything stops" is the existing `refused` helper (:881 of the test file): exit 3, the exact message, no unit, wrapper, pointer or store change, no start or stop, and one log line with `result: refused`. The other change to the test file: `proofFile` takes an optional fourth argument, the directory, with the old `e.root` as its default, so tests 3 and 4 can put a watched file inside the fixture REPO. Each of those subtests removes its `proof-r188` directory in `t.after`. `linkSync` joins the `node:fs` import. The uid case is out of reach without root, as the brief says, and has no test. ### Readings of the brief 1. "No log line" in brief test 1. A precheck refusal in a real run writes one log line, `result: refused` with the code, as every refusal does since the log began (T53 test 5 and every other `refused` case pin it). I read the brief as no activation line, which `refused` pins: one line, `refused`, and no unit or pointer change. Changing that would need a script change, which this row can't make. 2. Removal and the symlink swap keep a hard link to the old file under `e.root` before they unlink its path. The read watch is on the inode. When a watched inode is freed, inotify sends `IN_IGNORED`, which no mask filters out, and the watch counts it as an event. A probe of the T53 watch counted 1 after a plain unlink and 0 with a hard link kept. The script sees the same thing either way: the path is gone, or it is a symlink. ## Results All runs are from `~/darkwing-scratch/r188/run188.sh`, in a git worktree of 525990ecb plus the candidate, with TMPDIR under `~/darkwing-scratch`. The runs took 10:38:55Z-10:46:50Z. - The whole `stack-release.test.mjs` passes 151/151 (144 before this row, plus T54 and its six subtests). - Control: on the unchanged script (blob 2586a2a37cd2), T53 and T54 pass 19/19. The brief's base control is this run. The row changes no script, so base and candidate run the same script, and the new tests must pass on it. - Mutants. The runner runs T53 and T54 under each one, then puts the script back and checks its sha256. The restore matched every time, and the worktree script is blob 2586a2a37cd2 after the run. | Mutant | Replacement | Fails | Failing subtests | |---|---|---|---| | M8 | `return 0` before the `fi` that ends the carried branch (:738) | 5 | T54 test 1 (all three changes) and test 2 | | M9 | `"$REPO" \| "$REPO"/* \| ` dropped from the realpath case (:775) | 3 | T54 tests 3 and 4 | | M10 | `why=""; [ -z "$PROOF_HEADER" ] \|\| why="$(proof_file_problem "$2")"` (:759) | 5 | T54 test 1 (all three changes) and test 2 | The fail counts include the T54 parent. Each failing subtest stops at its first `--plan` (exit 0, not 3). T53 passes under all three mutants, which matches Filbert's finding that M8 and M9 survived it. The new tests are what kill them. ## Candidate - `candidate-manifest.sha256` (sha256 86aa549bad75...): `d38a2f9d6e4ece57907e95b9c24601ec823cfab8d22ab783a8ee64c79104100b packages/cli/tests/stack-release.test.mjs` - `build.patch` (sha256 db7e41b2ba76..., 85 lines) applies on 525990ecb, and `sha256sum -c` of the manifest passes on the result. ## For Sage's gate - The one changed path is `packages/cli/tests/stack-release.test.mjs`. No test or script reads it as a file (`grep -rln stack-release.test packages/*/tests scripts/test-*.sh` finds nothing), and no file pins the base blob's sha256 (95e44c2f...). - Run the cli suite and every `scripts/test-*.sh` from a git worktree of the candidate, as the brief says.