#!/usr/bin/env bash # Usage-error contract for pr-close.sh (R1/R4, 2026-08-28). # # R4: usage errors print to STDERR and exit 2, distinct from provider, # credential, and verification failures (exit 1). R1: -b/--body is the # canonical comment flag; -c/--comment remains a compatible alias. # The comment is OPTIONAL here (an issue may close without one), so unlike # issue-comment there is no missing-comment arm. # # 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/pr-close-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/pr-close.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/pr-close.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: pr-close.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 "unknown option" "unknown option names itself on stderr" # 3. Missing required PR number: rc 2, stderr. expect_rc 2 "missing -n exits 2" expect_stderr "PR number is required" "missing -n message on stderr" # 4. Value-less flags: rc 2 with "requires a value" on stderr. for flag in -n -b -c --number --body --comment; 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" -n 5 -b --help expect_rc 2 "short flag value rejected" -n 5 -b -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 -b -c; do rc=0 run_wrapper_sandboxed -n 5 "$flag" "closing note" >"$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 "pr-close.sh usage-contract regression passed (R1/R4)"