#!/usr/bin/env bash # Usage-error contract for issue-view.sh (R4, 2026-08-28). # # issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, # -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error # contract, value checks, and the no-provider-contact proof. Required: -i. # # Arms: # 1. --help and -h exit 0 and print usage. # 2. Unknown option exits 2 with the message on stderr. # 3. Missing required -i exits 2 (stderr). # 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). # 5. -b and -c both pass parsing (sandboxed runner: the run then fails # at credential resolution, nonzero and NOT 2) — no real token is # ever read and no provider is contacted. # 6. No arm performs any provider request (PATH shims record every # invocation; the probe log must stay empty). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-view-usage}" BIN_DIR="$WORK_DIR/bin" PROBE_LOG="$WORK_DIR/provider-probes.log" OUT_FILE="$WORK_DIR/out.log" ERR_FILE="$WORK_DIR/err.log" cleanup() { rm -rf "$WORK_DIR" } trap cleanup EXIT mkdir -p "$BIN_DIR" : > "$PROBE_LOG" # Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an # API fallback that treats a successful curl as a closed PR, so exit-0 # stubs would let the sandbox arms "succeed" (measured 2026-08-28). for tool in gh tea curl; do cat > "$BIN_DIR/$tool" <> "$PROBE_LOG" exit 99 STUB chmod +x "$BIN_DIR/$tool" done run_wrapper() { ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-view.sh" "$@" ) } # Hermetic variant: neutralizes every identity/credential source the wrapper # consults so parse-acceptance arms fail at credential resolution in ANY cwd # repo (see test-issue-comment-usage-contract.sh for the measured incident). run_wrapper_sandboxed() { mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" ( cd "$WORK_DIR" PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ "$SCRIPT_DIR/issue-view.sh" "$@" ) } fail() { echo "FAIL: $*" >&2 echo "--- stderr ---" >&2 cat "$ERR_FILE" >&2 exit 1 } expect_rc() { # expect_rc local want="$1" desc="$2" rc=0 shift 2 run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" } expect_stderr() { # expect_stderr grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" } # 1. Help exits 0 and prints usage. expect_rc 0 "--help exits 0" --help grep -q "Usage: issue-view.sh" "$OUT_FILE" || fail "--help did not print usage" expect_rc 0 "-h exits 0" -h # 2. Unknown option: rc 2, stderr. expect_rc 2 "unknown option exits 2" --bogus expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" # 3. Missing required PR number: rc 2, stderr. expect_rc 2 "missing -i exits 2" expect_stderr "Issue number is required" "missing -i message on stderr" # 4. Value-less flags: rc 2 with "requires a value" on stderr. for flag in -i --issue; do expect_rc 2 "value-less $flag exits 2" "$flag" expect_stderr "requires a value" "value-less $flag message on stderr" done # 4a. An option-like value is a MISSING value, not a value (codex PR #1464: # -b --help previously consumed --help as the body and performed the write). expect_rc 2 "option-like value rejected" -i --help expect_rc 2 "short flag value rejected" -i -h expect_stderr "requires a value" "short flag value message on stderr" expect_stderr "requires a value" "option-like value message on stderr" # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 cat "$PROBE_LOG" >&2 exit 1 fi # 5. Alias acceptance under the sandbox: both -b and -c carry a value past # parsing; the run fails at credential resolution nonzero and NOT 2. for flag in -i; do rc=0 run_wrapper_sandboxed -i 5 >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" done # 6. Post-sandbox provider assertions are intentionally NOT applied here: # pr-close's gitea path attempts a tea WRITE (tea pr comment) when a # comment parses, then falls back to the API. Hermeticity for this # wrapper comes from the FAILING stubs (exit 99), not from non-contact — # the arm above proves only parse acceptance and non-usage classification. # Parser-failure arms (1-4) remain zero-contact (asserted at 4b). echo "issue-view.sh usage-contract regression passed (R1/R4)"