From d144ae379cfab862f35d0cb257cc6f714c993fa2 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Mon, 20 Jul 2026 09:58:19 +0000 Subject: [PATCH] test: widen the read-guard sweep to bin/ and to plain-statement reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- test/cli.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/cli.sh b/test/cli.sh index 8594117..ec6c0f3 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -817,6 +817,30 @@ check "runner remove: headless token prompt refuses loudly" 0 "" \ # so a bare `read -rsp` (no `||` on its line) must not exist anywhere. check "prompts: no bare read -rsp remains" 1 "" \ grep -RE 'read -rsp[^|]*$' "$ROOT/commands/" +# ...and the same sweep, widened on the two axes #68 escaped through (#75). +# That check reads `-rsp` literally and scans commands/ only; #68 was a plain +# `read -r reply` in bin/rig, so it missed on the spelling AND on the path. +# 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 — silently, with exit 1, indistinguishable +# from a normal refusal. +# +# So: match `read` at the start of a statement (leading whitespace only), +# whatever its flags or arity, then subtract the two shapes that are safe by +# construction: +# `||` — the guard itself (`|| die`, `|| reply=""`, `|| { echo; die … }`). +# An errexit-exempt read, which is the whole cure. +# `<<<` — a here-string always supplies a terminating newline, so the read +# cannot return non-zero. lib/users-config.sh:50/:78 are these. +# `while`/`until`/`if` heads need no subtraction: the anchor already excludes +# them, since `read` is not the first word on those lines. Keep the guard on +# the read's own line — a `\`-continued `||` reads as unguarded here, by +# design, because it is not visible at the point of failure. +unguarded_read() { + grep -REn '^[[:space:]]*read[[:space:]]' "$ROOT/bin/" "$ROOT/commands/" \ + | grep -Ev '\|\||<<<' +} +check "prompts: no unguarded plain-statement read remains (#75)" 1 "" unguarded_read # --- runner install: --repo must agree with what the box is already on ------- # The bug: `install --repo B` on a box registered to repo A skipped configure,