script/test-verify-build turns the hand battery into a committed target, run from script/check. 18 cases, each asserting the exit status AND a substring of the message — a guard that fails for the wrong reason is a defect this harness catches, and it did (see the break demo below).
Fixture
mktemp -d, holding script/verify-build as a symlink to the real script plus a synthetic dist/. verify-build derives ROOT from dirname "$0"/.., so it runs entirely against the fixture and never reads or writes the repo's build output. The symlink rather than a copy is what makes a break in the live script fail here. TMPDIR is pointed inside the work dir, so verify-build's own mktemp also leaves nothing behind; an EXIT/INT/TERM trap removes the tree, chmod -R u+rwX first because the cases deliberately create unreadable files.
Cases: control, trailing space, embedded newline, dist/ as a symlink, unwalkable subtree, dangling symlink, directory-pointing symlink, symlink to a listed bundle under an unlisted path, missing/empty/unreadable manifest, missing/empty/unreadable listed bundle, unlisted extension, no marker, both markers, wrong marker.
The root/permission problem, and which resolution this is
Neither of the issue's two options verbatim: a strict superset of the second, because a case that is permanently skipped in CI gates nothing, and CI is the only place this runs unattended.
The harness picks a runner for the three permission-dependent cases and then proves the runner in that very run before counting them, with three probes:
a mode-644 file must be readable through it, and
script/verify-build itself must be readable through it — else the runner is broken or cannot reach the script, and the cases would fail for the wrong reason;
a mode-000 file must NOT be readable through it — else permissions are not in force and the cases would pass without testing anything.
Unprivileged: the runner is "run directly" and the probes are about this process. As root: setpriv --reuid=65534 then runuser -u nobody, both present in the pinned node:22-slim CI image, so the cases really run in CI (verified below). Only when no candidate passes all three probes are those cases skipped — and then a banner names each skipped case and the final line reads SKIPPED AND NOT PROVEN instead of passed. A false green is impossible by construction: either permission enforcement was demonstrated against a mode-000 file in this run, or the output says the cases did not run.
Why not a non-root USER in the Dockerfile, the "honest" option: COPY . . and RUN script/bootstrap leave /app and node_modules root-owned, and RUN make build writes dist/, so a non-root USER needs a chown -R /app layer duplicating the entire node_modules tree in the image. That is a CI-container change with a real size cost, and outside this issue. Happy to file it separately if wanted.
Proof both branches behave, in the pinned CI image as uid 0:
$ docker run --rm -v ...:/app:ro -w /app node@sha256:5373f19...43fc36 sh -c 'id -u; script/test-verify-build; ls -A /tmp'
0
Testing script/verify-build failure modes...
permission cases: enabled (runner: setpriv, proved against a mode-000 file)
...
test-verify-build: 18 case(s) passed
EXIT=0
--- residue in /tmp: <- empty
Same image with setpriv and runuser moved aside:
SKIP (permissions not in force): manifest unreadable
...
################################################################################
## WARNING: 3 PERMISSION CASE(S) DID NOT RUN, AND THIS RUN DOES NOT
## PROVE THEM. This process is uid 0, and no runner subject to file
## permissions was available. Tried: setpriv, runuser.
## Under root, chmod 000 stops neither find nor grep, so these cases would
## have passed without testing anything. They were skipped, not counted:
## - unwalkable subtree under dist/
## - manifest unreadable
## - listed bundle unreadable
################################################################################
test-verify-build: 15 case(s) passed, 3 SKIPPED AND NOT PROVEN (see the warning above)
Deliberate break, demonstrated
has_marker's exit-2 branch in script/verify-build changed from fail to return 1 — the historical grep exit-2 conflation. Reverted after capture; the committed script is unmodified.
Testing script/verify-build failure modes...
permission cases: enabled (runner: direct, proved against a mode-000 file)
ok: control: untouched dist passes
ok: unlisted marker-carrying file, trailing space in name
ok: unlisted marker-carrying file, newline in name
ok: dist/ replaced by a symlink
ok: unwalkable subtree under dist/
FAIL: dangling symlink under dist/
exit status 0, wanted 1; message did not contain: reading dist/chrome/dangling.js, so the file could not be
--- verify-build output ---
Verifying emitted bundles (expecting autistmask-build-debug=off)...
ok: dist/chrome/src/popup/index.js (autistmask-build-debug=off)
ok: dist/firefox/src/popup/index.js (autistmask-build-debug=off)
grep: dist/chrome/dangling.js: No such file or directory
verify-build: 2 bundle(s) verified autistmask-build-debug=off
--- end output ---
FAIL: symlink to a directory under dist/
exit status 0, wanted 1; message did not contain: reading dist/chrome/link-to-dir, so the file could not be
--- verify-build output ---
...
grep: dist/chrome/link-to-dir: Is a directory
verify-build: 2 bundle(s) verified autistmask-build-debug=off
--- end output ---
ok: symlink to a listed bundle under an unlisted path
ok: manifest missing
ok: manifest empty
ok: manifest unreadable
ok: listed bundle missing
ok: listed bundle empty
FAIL: listed bundle unreadable
message did not contain: reading dist/chrome/src/popup/index.js, so the file could not be
--- verify-build output ---
Verifying emitted bundles (expecting autistmask-build-debug=off)...
grep: dist/chrome/src/popup/index.js: Permission denied
verify-build: FAIL: dist/chrome/src/popup/index.js carries no debug marker, so its DEBUG state cannot be
determined. Either BUILD_DEBUG_MARKER is gone from src/shared/constants.js
or the emitted output changed shape. Refusing to report success.
--- end output ---
ok: unlisted extension carrying a marker
ok: listed bundle carries no marker
ok: listed bundle carries both markers
ok: wrong marker for the requested mode
test-verify-build: 3 case(s) FAILED, 15 passed, 0 skipped
Exit 1, so make check fails. Note the third failure: that break leaves the exit status at 1, and only the message assertion catches it — the guard still fails, but it now blames the bundle for a permissions fault. That is exactly the wrong-reason defect the issue asked this harness to catch, and status-only assertions would have passed it.
Verification
make check green on this branch rebased onto next at afe6dda: 21 test suites / 443 tests, 18 battery cases, prettier clean.
The deliberate break above was re-run against this rebased head: make check exits non-zero, test-verify-build: 3 case(s) FAILED, 15 passed, 0 skipped. Restored, green again, git status clean.
docker build --no-cache . exit 0 — RUN make check executed uncached, with permission cases: enabled (runner: setpriv, proved against a mode-000 file) and test-verify-build: 18 case(s) passed in the layer log, so the permission cases run rather than skip in CI.
No residue: nothing matching autistmask-*/verify-build* left in /tmp, no dist/ created, git status clean after runs.
Runtime cost
0.705s locally, 0.32s in the CI container. Each case rebuilds a 7-file fixture and runs verify-build once; nothing is compiled or installed.
Closes [#227](https://git.eeqj.de/sneak/AutistMask/issues/227).
`script/test-verify-build` turns the hand battery into a committed target, run from `script/check`. 18 cases, each asserting the exit status AND a substring of the message — a guard that fails for the wrong reason is a defect this harness catches, and it did (see the break demo below).
## Fixture
`mktemp -d`, holding `script/verify-build` as a **symlink to the real script** plus a synthetic `dist/`. `verify-build` derives `ROOT` from `dirname "$0"/..`, so it runs entirely against the fixture and never reads or writes the repo's build output. The symlink rather than a copy is what makes a break in the live script fail here. `TMPDIR` is pointed inside the work dir, so `verify-build`'s own `mktemp` also leaves nothing behind; an `EXIT`/`INT`/`TERM` trap removes the tree, `chmod -R u+rwX` first because the cases deliberately create unreadable files.
Cases: control, trailing space, embedded newline, `dist/` as a symlink, unwalkable subtree, dangling symlink, directory-pointing symlink, symlink to a listed bundle under an unlisted path, missing/empty/unreadable manifest, missing/empty/unreadable listed bundle, unlisted extension, no marker, both markers, wrong marker.
## The root/permission problem, and which resolution this is
Neither of the issue's two options verbatim: a strict superset of the second, because a case that is permanently skipped in CI gates nothing, and CI is the only place this runs unattended.
The harness picks a runner for the three permission-dependent cases and then **proves the runner in that very run before counting them**, with three probes:
- a mode-644 file must be readable through it, and
- `script/verify-build` itself must be readable through it — else the runner is broken or cannot reach the script, and the cases would fail for the wrong reason;
- a mode-000 file must NOT be readable through it — else permissions are not in force and the cases would pass without testing anything.
Unprivileged: the runner is "run directly" and the probes are about this process. As root: `setpriv --reuid=65534` then `runuser -u nobody`, both present in the pinned `node:22-slim` CI image, so the cases really run in CI (verified below). Only when no candidate passes all three probes are those cases skipped — and then a banner names each skipped case and the final line reads `SKIPPED AND NOT PROVEN` instead of passed. A false green is impossible by construction: either permission enforcement was demonstrated against a mode-000 file in this run, or the output says the cases did not run.
Why not a non-root `USER` in the Dockerfile, the "honest" option: `COPY . .` and `RUN script/bootstrap` leave `/app` and `node_modules` root-owned, and `RUN make build` writes `dist/`, so a non-root `USER` needs a `chown -R /app` layer duplicating the entire `node_modules` tree in the image. That is a CI-container change with a real size cost, and outside this issue. Happy to file it separately if wanted.
Proof both branches behave, in the pinned CI image as uid 0:
```
$ docker run --rm -v ...:/app:ro -w /app node@sha256:5373f19...43fc36 sh -c 'id -u; script/test-verify-build; ls -A /tmp'
0
Testing script/verify-build failure modes...
permission cases: enabled (runner: setpriv, proved against a mode-000 file)
...
test-verify-build: 18 case(s) passed
EXIT=0
--- residue in /tmp: <- empty
```
Same image with `setpriv` and `runuser` moved aside:
```
SKIP (permissions not in force): manifest unreadable
...
################################################################################
## WARNING: 3 PERMISSION CASE(S) DID NOT RUN, AND THIS RUN DOES NOT
## PROVE THEM. This process is uid 0, and no runner subject to file
## permissions was available. Tried: setpriv, runuser.
## Under root, chmod 000 stops neither find nor grep, so these cases would
## have passed without testing anything. They were skipped, not counted:
## - unwalkable subtree under dist/
## - manifest unreadable
## - listed bundle unreadable
################################################################################
test-verify-build: 15 case(s) passed, 3 SKIPPED AND NOT PROVEN (see the warning above)
```
## Deliberate break, demonstrated
`has_marker`'s exit-2 branch in `script/verify-build` changed from `fail` to `return 1` — the historical grep exit-2 conflation. Reverted after capture; the committed script is unmodified.
```
Testing script/verify-build failure modes...
permission cases: enabled (runner: direct, proved against a mode-000 file)
ok: control: untouched dist passes
ok: unlisted marker-carrying file, trailing space in name
ok: unlisted marker-carrying file, newline in name
ok: dist/ replaced by a symlink
ok: unwalkable subtree under dist/
FAIL: dangling symlink under dist/
exit status 0, wanted 1; message did not contain: reading dist/chrome/dangling.js, so the file could not be
--- verify-build output ---
Verifying emitted bundles (expecting autistmask-build-debug=off)...
ok: dist/chrome/src/popup/index.js (autistmask-build-debug=off)
ok: dist/firefox/src/popup/index.js (autistmask-build-debug=off)
grep: dist/chrome/dangling.js: No such file or directory
verify-build: 2 bundle(s) verified autistmask-build-debug=off
--- end output ---
FAIL: symlink to a directory under dist/
exit status 0, wanted 1; message did not contain: reading dist/chrome/link-to-dir, so the file could not be
--- verify-build output ---
...
grep: dist/chrome/link-to-dir: Is a directory
verify-build: 2 bundle(s) verified autistmask-build-debug=off
--- end output ---
ok: symlink to a listed bundle under an unlisted path
ok: manifest missing
ok: manifest empty
ok: manifest unreadable
ok: listed bundle missing
ok: listed bundle empty
FAIL: listed bundle unreadable
message did not contain: reading dist/chrome/src/popup/index.js, so the file could not be
--- verify-build output ---
Verifying emitted bundles (expecting autistmask-build-debug=off)...
grep: dist/chrome/src/popup/index.js: Permission denied
verify-build: FAIL: dist/chrome/src/popup/index.js carries no debug marker, so its DEBUG state cannot be
determined. Either BUILD_DEBUG_MARKER is gone from src/shared/constants.js
or the emitted output changed shape. Refusing to report success.
--- end output ---
ok: unlisted extension carrying a marker
ok: listed bundle carries no marker
ok: listed bundle carries both markers
ok: wrong marker for the requested mode
test-verify-build: 3 case(s) FAILED, 15 passed, 0 skipped
```
Exit 1, so `make check` fails. Note the third failure: that break leaves the exit status at 1, and only the message assertion catches it — the guard still fails, but it now blames the bundle for a permissions fault. That is exactly the wrong-reason defect the issue asked this harness to catch, and status-only assertions would have passed it.
## Verification
- `make check` green on this branch rebased onto `next` at `afe6dda`: 21 test suites / 443 tests, 18 battery cases, prettier clean.
- The deliberate break above was re-run against this rebased head: `make check` exits non-zero, `test-verify-build: 3 case(s) FAILED, 15 passed, 0 skipped`. Restored, green again, `git status` clean.
- `docker build --no-cache .` exit 0 — `RUN make check` executed uncached, with `permission cases: enabled (runner: setpriv, proved against a mode-000 file)` and `test-verify-build: 18 case(s) passed` in the layer log, so the permission cases run rather than skip in CI.
- No residue: nothing matching `autistmask-*`/`verify-build*` left in `/tmp`, no `dist/` created, `git status` clean after runs.
## Runtime cost
0.705s locally, 0.32s in the CI container. Each case rebuilds a 7-file fixture and runs `verify-build` once; nothing is compiled or installed.
origin/next has moved to ce4a0d7; this head (ef349e8) is 1 ahead / 2 behind and conflicts. Gitea's mergeable: true was computed against ba35282 and is stale.
$ git rebase origin/next
Auto-merging README.md
Auto-merging TODO.md
CONFLICT (content): Merge conflict in TODO.md
error: could not apply ef349e8... test: cover every verify-build failure mode (closes #227)
TODO.md: this commit and #230 both insert a bullet at the top of # Completed Steps. Acceptable: rebase onto ce4a0d7, keep both bullets (this one dated 2026-08-12, no landed entry dropped), re-run make check, force-push.
2. Commit message overstates the permission coverage (fix in the same rebase)
The commit body says "before counting the five permission-dependent cases"; the PR body and the plan comment on #227 repeat it. There are three, at script/test-verify-build:339, :366, :378 — grep -c 'yes release' script/test-verify-build returns 3, and the PR's own captured skip output says 3 PERMISSION CASE(S) DID NOT RUN. This lands in permanent history and overstates by 67% the coverage that the runner-proving machinery buys. Acceptable: say "three" in the commit body (and correct the PR body).
Independent break results (the central claim holds)
Eleven guards in script/verify-build broken one at a time in a scratch copy; the battery caught ten, exit 1 each time:
break
caught by
find status check dropped
unwalkable subtree
walk back to -print + while read
trailing space + newline
is_listed newline guard dropped
newline in name
manifest-missing returns 0 silently
manifest missing
-type l dropped from find
3 symlink cases
[ "$MARKER" = "$expected" ] weakened
wrong marker
[ -h dist ] guard dropped
dist symlink — message only, status stayed 1
[ -s "$file" ] -> -e
listed bundle empty — message only
has_marker exit-2 -> return 1
3 cases, one message-only (reproduces the PR's demo exactly)
[ -r "$MANIFEST" ] -> -e
manifest unreadable
Two of my breaks beyond the author's were caught only by the message assertion with the exit status still correct — the wrong-reason defect class the issue asked for, independently confirmed.
Not caught: is_listed's exit-2 branch (script/verify-build:98). Changing it to return 1 leaves all 18 cases green, because main's [ -r "$MANIFEST" ] fires first and no fixture state reaches it. Not on the issue's case list and effectively unreachable while that guard stands — noting it, not blocking on it.
Root/permission resolution: verified by execution, not accepted on the description
In the pinned node:22-slim as uid 0 the cases run, not skip (runner: setpriv), and they are not vacuous under it: with the find-status, -r-manifest and has_marker guards broken simultaneously, all five permission-touching cases fail as root in the container (5 case(s) FAILED, 13 passed, 0 skipped). Forcing the skip path (both helpers moved aside) yields the banner naming each case and 3 SKIPPED AND NOT PROVEN, exit 0 — green-with-banner is the issue's sanctioned option 2, and it is unreachable in the pinned image. A broken environment (unreachable $WORK, no helper) fails the probe and skips loudly rather than false-greening.
Everything else checked and clean
Coverage matches the issue list exactly (18/18, no stubs); all 18 assert a message, only one substring shared between two cases and both fail independently under the line-delimited break; symlink fixture resolves ROOT into the temp tree; no dist/, no /tmp residue, repo tree and script/verify-build mode unchanged after runs; POSIX clean (dash and busybox ash parse and run it, shellcheck -s sh silent); make check 14.0s locally with the battery at 0.78s; script/cibuild rc=0 with RUN make checkexecuted (26.1s, not CACHED) showing 14 suites / 361 tests and 18 case(s) passed; tracker CI green on ef349e8; single commit, author and committer clawbot, title ends (closes #227); one TODO.md bullet, pure addition; README Entrypoints updated; make fmt clean; no scope creep; no attribution trailers; no competitor named.
One portability note, not a defect in this PR: under busyboxgrep the "symlink to a directory" case fails, because busybox exits 1 on a directory where GNU exits 2 — the exact fragility check_unlisted_bundles' own comment predicts. Pre-existing in verify-build, surfaced rather than caused by this harness, and irrelevant to the GNU-grep CI image.
FAIL — `needs-rebase`.
## 1. Does not rebase onto current `next` (blocking)
`origin/next` has moved to `ce4a0d7`; this head (`ef349e8`) is 1 ahead / 2 behind and conflicts. Gitea's `mergeable: true` was computed against `ba35282` and is stale.
```
$ git rebase origin/next
Auto-merging README.md
Auto-merging TODO.md
CONFLICT (content): Merge conflict in TODO.md
error: could not apply ef349e8... test: cover every verify-build failure mode (closes #227)
```
`TODO.md`: this commit and [#230](https://git.eeqj.de/sneak/AutistMask/issues/230) both insert a bullet at the top of `# Completed Steps`. Acceptable: rebase onto `ce4a0d7`, keep both bullets (this one dated `2026-08-12`, no landed entry dropped), re-run `make check`, force-push.
## 2. Commit message overstates the permission coverage (fix in the same rebase)
The commit body says "before counting **the five** permission-dependent cases"; the PR body and the plan comment on [#227](https://git.eeqj.de/sneak/AutistMask/issues/227#issuecomment-57836) repeat it. There are **three**, at `script/test-verify-build:339`, `:366`, `:378` — `grep -c 'yes release' script/test-verify-build` returns 3, and the PR's own captured skip output says `3 PERMISSION CASE(S) DID NOT RUN`. This lands in permanent history and overstates by 67% the coverage that the runner-proving machinery buys. Acceptable: say "three" in the commit body (and correct the PR body).
## Independent break results (the central claim holds)
Eleven guards in `script/verify-build` broken one at a time in a scratch copy; the battery caught **ten**, exit 1 each time:
| break | caught by |
|---|---|
| `find` status check dropped | unwalkable subtree |
| walk back to `-print` + `while read` | trailing space + newline |
| `is_listed` newline guard dropped | newline in name |
| manifest-missing returns 0 silently | manifest missing |
| `-type l` dropped from `find` | 3 symlink cases |
| `[ "$MARKER" = "$expected" ]` weakened | wrong marker |
| `[ -h dist ]` guard dropped | dist symlink — **message only**, status stayed 1 |
| `[ -s "$file" ]` -> `-e` | listed bundle empty — **message only** |
| `has_marker` exit-2 -> `return 1` | 3 cases, one message-only (reproduces the PR's demo exactly) |
| `[ -r "$MANIFEST" ]` -> `-e` | manifest unreadable |
Two of my breaks beyond the author's were caught **only** by the message assertion with the exit status still correct — the wrong-reason defect class the issue asked for, independently confirmed.
Not caught: `is_listed`'s exit-2 branch (`script/verify-build:98`). Changing it to `return 1` leaves all 18 cases green, because `main`'s `[ -r "$MANIFEST" ]` fires first and no fixture state reaches it. Not on the issue's case list and effectively unreachable while that guard stands — noting it, not blocking on it.
## Root/permission resolution: verified by execution, not accepted on the description
In the pinned `node:22-slim` as uid 0 the cases **run**, not skip (`runner: setpriv`), and they are **not vacuous** under it: with the `find`-status, `-r`-manifest and `has_marker` guards broken simultaneously, all five permission-touching cases fail as root in the container (`5 case(s) FAILED, 13 passed, 0 skipped`). Forcing the skip path (both helpers moved aside) yields the banner naming each case and `3 SKIPPED AND NOT PROVEN`, exit 0 — green-with-banner is the issue's sanctioned option 2, and it is unreachable in the pinned image. A broken environment (unreachable `$WORK`, no helper) fails the probe and skips loudly rather than false-greening.
## Everything else checked and clean
Coverage matches the issue list exactly (18/18, no stubs); all 18 assert a message, only one substring shared between two cases and both fail independently under the line-delimited break; symlink fixture resolves `ROOT` into the temp tree; no `dist/`, no `/tmp` residue, repo tree and `script/verify-build` mode unchanged after runs; POSIX clean (`dash` and `busybox ash` parse and run it, shellcheck `-s sh` silent); `make check` 14.0s locally with the battery at 0.78s; `script/cibuild` rc=0 with `RUN make check` **executed** (26.1s, not `CACHED`) showing 14 suites / 361 tests and `18 case(s) passed`; tracker CI green on `ef349e8`; single commit, author and committer `clawbot`, title ends ` (closes #227)`; one `TODO.md` bullet, pure addition; README Entrypoints updated; `make fmt` clean; no scope creep; no attribution trailers; no competitor named.
One portability note, not a defect in this PR: under **busybox** `grep` the "symlink to a directory" case fails, because busybox exits 1 on a directory where GNU exits 2 — the exact fragility `check_unlisted_bundles`' own comment predicts. Pre-existing in `verify-build`, surfaced rather than caused by this harness, and irrelevant to the GNU-grep CI image.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #227.
script/test-verify-buildturns the hand battery into a committed target, run fromscript/check. 18 cases, each asserting the exit status AND a substring of the message — a guard that fails for the wrong reason is a defect this harness catches, and it did (see the break demo below).Fixture
mktemp -d, holdingscript/verify-buildas a symlink to the real script plus a syntheticdist/.verify-buildderivesROOTfromdirname "$0"/.., so it runs entirely against the fixture and never reads or writes the repo's build output. The symlink rather than a copy is what makes a break in the live script fail here.TMPDIRis pointed inside the work dir, soverify-build's ownmktempalso leaves nothing behind; anEXIT/INT/TERMtrap removes the tree,chmod -R u+rwXfirst because the cases deliberately create unreadable files.Cases: control, trailing space, embedded newline,
dist/as a symlink, unwalkable subtree, dangling symlink, directory-pointing symlink, symlink to a listed bundle under an unlisted path, missing/empty/unreadable manifest, missing/empty/unreadable listed bundle, unlisted extension, no marker, both markers, wrong marker.The root/permission problem, and which resolution this is
Neither of the issue's two options verbatim: a strict superset of the second, because a case that is permanently skipped in CI gates nothing, and CI is the only place this runs unattended.
The harness picks a runner for the three permission-dependent cases and then proves the runner in that very run before counting them, with three probes:
script/verify-builditself must be readable through it — else the runner is broken or cannot reach the script, and the cases would fail for the wrong reason;Unprivileged: the runner is "run directly" and the probes are about this process. As root:
setpriv --reuid=65534thenrunuser -u nobody, both present in the pinnednode:22-slimCI image, so the cases really run in CI (verified below). Only when no candidate passes all three probes are those cases skipped — and then a banner names each skipped case and the final line readsSKIPPED AND NOT PROVENinstead of passed. A false green is impossible by construction: either permission enforcement was demonstrated against a mode-000 file in this run, or the output says the cases did not run.Why not a non-root
USERin the Dockerfile, the "honest" option:COPY . .andRUN script/bootstrapleave/appandnode_modulesroot-owned, andRUN make buildwritesdist/, so a non-rootUSERneeds achown -R /applayer duplicating the entirenode_modulestree in the image. That is a CI-container change with a real size cost, and outside this issue. Happy to file it separately if wanted.Proof both branches behave, in the pinned CI image as uid 0:
Same image with
setprivandrunusermoved aside:Deliberate break, demonstrated
has_marker's exit-2 branch inscript/verify-buildchanged fromfailtoreturn 1— the historical grep exit-2 conflation. Reverted after capture; the committed script is unmodified.Exit 1, so
make checkfails. Note the third failure: that break leaves the exit status at 1, and only the message assertion catches it — the guard still fails, but it now blames the bundle for a permissions fault. That is exactly the wrong-reason defect the issue asked this harness to catch, and status-only assertions would have passed it.Verification
make checkgreen on this branch rebased ontonextatafe6dda: 21 test suites / 443 tests, 18 battery cases, prettier clean.make checkexits non-zero,test-verify-build: 3 case(s) FAILED, 15 passed, 0 skipped. Restored, green again,git statusclean.docker build --no-cache .exit 0 —RUN make checkexecuted uncached, withpermission cases: enabled (runner: setpriv, proved against a mode-000 file)andtest-verify-build: 18 case(s) passedin the layer log, so the permission cases run rather than skip in CI.autistmask-*/verify-build*left in/tmp, nodist/created,git statusclean after runs.Runtime cost
0.705s locally, 0.32s in the CI container. Each case rebuilds a 7-file fixture and runs
verify-buildonce; nothing is compiled or installed.FAIL —
needs-rebase.1. Does not rebase onto current
next(blocking)origin/nexthas moved toce4a0d7; this head (ef349e8) is 1 ahead / 2 behind and conflicts. Gitea'smergeable: truewas computed againstba35282and is stale.TODO.md: this commit and #230 both insert a bullet at the top of# Completed Steps. Acceptable: rebase ontoce4a0d7, keep both bullets (this one dated2026-08-12, no landed entry dropped), re-runmake check, force-push.2. Commit message overstates the permission coverage (fix in the same rebase)
The commit body says "before counting the five permission-dependent cases"; the PR body and the plan comment on #227 repeat it. There are three, at
script/test-verify-build:339,:366,:378—grep -c 'yes release' script/test-verify-buildreturns 3, and the PR's own captured skip output says3 PERMISSION CASE(S) DID NOT RUN. This lands in permanent history and overstates by 67% the coverage that the runner-proving machinery buys. Acceptable: say "three" in the commit body (and correct the PR body).Independent break results (the central claim holds)
Eleven guards in
script/verify-buildbroken one at a time in a scratch copy; the battery caught ten, exit 1 each time:findstatus check dropped-print+while readis_listednewline guard dropped-type ldropped fromfind[ "$MARKER" = "$expected" ]weakened[ -h dist ]guard dropped[ -s "$file" ]->-ehas_markerexit-2 ->return 1[ -r "$MANIFEST" ]->-eTwo of my breaks beyond the author's were caught only by the message assertion with the exit status still correct — the wrong-reason defect class the issue asked for, independently confirmed.
Not caught:
is_listed's exit-2 branch (script/verify-build:98). Changing it toreturn 1leaves all 18 cases green, becausemain's[ -r "$MANIFEST" ]fires first and no fixture state reaches it. Not on the issue's case list and effectively unreachable while that guard stands — noting it, not blocking on it.Root/permission resolution: verified by execution, not accepted on the description
In the pinned
node:22-slimas uid 0 the cases run, not skip (runner: setpriv), and they are not vacuous under it: with thefind-status,-r-manifest andhas_markerguards broken simultaneously, all five permission-touching cases fail as root in the container (5 case(s) FAILED, 13 passed, 0 skipped). Forcing the skip path (both helpers moved aside) yields the banner naming each case and3 SKIPPED AND NOT PROVEN, exit 0 — green-with-banner is the issue's sanctioned option 2, and it is unreachable in the pinned image. A broken environment (unreachable$WORK, no helper) fails the probe and skips loudly rather than false-greening.Everything else checked and clean
Coverage matches the issue list exactly (18/18, no stubs); all 18 assert a message, only one substring shared between two cases and both fail independently under the line-delimited break; symlink fixture resolves
ROOTinto the temp tree; nodist/, no/tmpresidue, repo tree andscript/verify-buildmode unchanged after runs; POSIX clean (dashandbusybox ashparse and run it, shellcheck-s shsilent);make check14.0s locally with the battery at 0.78s;script/cibuildrc=0 withRUN make checkexecuted (26.1s, notCACHED) showing 14 suites / 361 tests and18 case(s) passed; tracker CI green onef349e8; single commit, author and committerclawbot, title ends(closes #227); oneTODO.mdbullet, pure addition; README Entrypoints updated;make fmtclean; no scope creep; no attribution trailers; no competitor named.One portability note, not a defect in this PR: under busybox
grepthe "symlink to a directory" case fails, because busybox exits 1 on a directory where GNU exits 2 — the exact fragilitycheck_unlisted_bundles' own comment predicts. Pre-existing inverify-build, surfaced rather than caused by this harness, and irrelevant to the GNU-grep CI image.ef349e8a39to07780c3d8c