test: widen the read-guard sweep to bin/ and to plain-statement reads #82

Merged
dan-claude-bot merged 2 commits from test/read-guard-sweep-bin-rig into main 2026-07-20 12:24:24 +00:00

2 commits

Author SHA1 Message Date
dan-claude-bot
d144ae379c test: widen the read-guard sweep to bin/ and to plain-statement reads
The #43 sweep (test/cli.sh:705) matched the literal `-rsp` spelling and
scanned commands/ only. #68 was a plain `read -r reply` in bin/rig, so it
missed on BOTH axes — the spelling and the path — and the bug survived
until a drill hit it.

The class is the shape, not the flags: any `read` run as a plain statement
under `set -euo pipefail` kills the shell at EOF, before the `case` that
would have printed the abort. The failure is silent and exits 1, which is
also what a normal refusal exits, so an exit-code assertion passes against
it.

The new sweep anchors `read` at the start of a statement across bin/ and
commands/, whatever its flags or arity, and subtracts only the two shapes
that are safe by construction: a `||` guard (the cure itself) and a `<<<`
here-string (which cannot return non-zero). `while`/`if ! ` heads need no
subtraction — the anchor already excludes them.

Guard against reintroduction, not a live fix: the tree is clean once #68's
one-token fix lands. Mutation-verified — a bare `read -r foo` planted in
bin/rig gives 404 passed, 1 failed, and the old #43 sweep stays green on
the same tree, which is precisely the gap being closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 12:18:51 +00:00
dan-claude-bot
880e95df21 fix: uninstall_confirm swallows Ctrl-D — the abort was silent
uninstall_confirm() read the operator's answer unguarded:

    read -r reply
    case "$reply" in y|Y|yes|YES|Yes) return 0 ;; *) die "aborted." ;; esac

bin/rig runs under `set -euo pipefail`, and both call sites (the
single-version and the --all confirms) invoke the function as a plain
statement — nothing suppresses errexit. Ctrl-D makes `read` return
non-zero, so the shell died AT THE READ and the case on the next line
was never evaluated: `die "aborted."` could not fire. The operator saw
the question, pressed Ctrl-D, and got nothing — no message, exit 1, at
exactly the moment the tool had asked whether to delete their install.

It failed closed, so nothing was ever wrongly removed; the damage was
that rig went silent at the one moment silence is unreadable.

The fix is `read -r reply || reply=""` — commands/db.sh:152's spelling
for the identical [y/N] confirm one file away. Empty routes through the
existing `*)` arm, so EOF aborts through the same path a bare Enter
already does: exactly one "aborted." message, no second die to keep in
sync.

test/cli.sh gains the first drills of the interactive path, which was
structurally untested (every existing uninstall check goes through
--force or RIG_YES, which is why this survived): `y` and Ctrl-D driven
through a real pty via util-linux `script`, guarded by a command -v
skip. They assert the MESSAGE, never the exit code — the unfixed code
also exits 1, so an exit-code assertion is green against the bug.

Mutation-verified: with `|| reply=""` reverted, 403 passed / 1 failed,
the single failure being `output missing 'aborted.'`; restored, 404
passed / 0 failed.

Refs #68
2026-07-20 12:18:33 +00:00