Files
stack/packages/mosaic/framework/tools/git/test-issue-comment-usage-contract.sh
T
marcie d326e6572a
ci/woodpecker/pr/ci Pipeline is pending
framework tools/git: strict value guards + provider-failure normalization family-wide (codex blockers on PR #1464)
- Value guards now reject option-like values: --anything always, and
  single-dash flag shapes (-h, -i). Previously -b --help consumed
  --help as the body and performed the write (codex example:
  issue-close -i --help proceeding to 'Closed GitHub issue #--help').
  Multi-char dash-leading text (-start of a list) stays a legal value.
- Every remaining direct provider exec (gh/tea/CMD arrays across
  issue-create/edit/assign/list/view, milestone-*, pr-create/edit,
  pr-close, issue-close/reopen) wrapped with rc capture and normalized
  to exit 1 with a stderr message — provider exit 2 no longer collides
  with the reserved usage-error status.
- All 16 usage-contract suites gained option-like and short-flag arms
  (16/16 green). Existing suites re-verified; test-pr-edit and
  test-issue-create-interactive-auth fail identically with these
  changes stashed (environment-coupled, not regressions; documented).
2026-08-28 18:26:10 -05:00

173 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Usage-error contract for issue-comment.sh (R1/R4 remediation, 2026-08-28).
#
# R4: usage errors print to STDERR and exit 2, distinct from provider,
# credential, and verification failures (exit 1), so a caller (or a stop gate)
# can tell an invocation defect from a delivery blocker. Before this contract
# the wrapper exited 1 for usage errors with messages on STDOUT, and a
# value-less flag (-c with no value) died SILENTLY at rc=1 because set -e
# killed the failed `shift 2`. That silent shape is what full-stopped a fleet
# seat: a caller could not distinguish "I invoked it wrong" from "delivery is
# blocked".
#
# R1: -b/--body is the canonical comment flag (matching issue-create,
# issue-edit, pr-create, pr-edit); -c/--comment remains a backward-compatible
# alias.
#
# 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. Missing required comment exits 2 (stderr).
# 5. A value-less flag (-i -b -c -l and long forms) exits 2 with a
# "requires a value" message on stderr (the former silent-death class).
# 6. -b and -c both pass parsing (the run then fails at platform detection
# in this non-repo fixture, nonzero and NOT 2), proving alias acceptance
# without any provider fixture.
# 7. No arm performs any provider request: PATH shims for gh/tea/curl
# record every invocation and 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-comment-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"
# Provider shims: any invocation is recorded and fails the run at the end.
# Usage-error arms must exit during argument parsing, before detect_platform,
# so these prove "no provider request on parser failure".
for tool in gh tea curl; do
cat > "$BIN_DIR/$tool" <<STUB
#!/usr/bin/env bash
echo "$tool \$*" >> "$PROBE_LOG"
# gh doubles as platform probe AND write path in arm 6b: probes exit 0; the
# comment write exits 2 (gh's own usage-error status) to prove the wrapper
# normalizes provider failures to exit 1 instead of propagating 2.
if [[ "\$1 \$2" == "issue comment" ]]; then exit 2; fi
exit 0
STUB
chmod +x "$BIN_DIR/$tool"
done
run_wrapper() {
( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-comment.sh" "$@" )
}
# Hermetic variant for parse-acceptance arms: neutralizes every identity/
# credential source the wrapper consults (seat env vars, HOME, XDG tea config)
# so the arm fails at credential resolution in ANY cwd repo, never reading a
# real token or contacting a provider. Measured 2026-08-28: without this, the
# arm's outcome depended on incidental URL-resolution state (brain cwd died at
# URL-not-found; a stack worktree cwd resolved a configured URL, read the real
# seat token, and invoked the curl stub — the suite then failed its own
# no-provider-contact check, correctly).
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-comment.sh" "$@"
)
}
fail() {
echo "FAIL: $*" >&2
echo "--- stderr ---" >&2
cat "$ERR_FILE" >&2
exit 1
}
expect_rc() { # expect_rc <want> <desc> <args...>
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 <pattern> <desc>
grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'"
}
# 1. Help exits 0 and prints usage on stdout.
expect_rc 0 "--help exits 0" --help
grep -q "Usage: issue-comment.sh" "$OUT_FILE" || fail "--help did not print usage"
expect_rc 0 "-h exits 0" -h
# 2. Unknown option: rc 2, message on stderr.
expect_rc 2 "unknown option exits 2" --bogus
expect_stderr "unknown option" "unknown option names itself on stderr"
# 3. Missing required issue number: rc 2, stderr.
expect_rc 2 "missing -i exits 2"
expect_stderr "issue number is required" "missing -i message on stderr"
# 4. Missing required comment: rc 2, stderr.
expect_rc 2 "missing comment exits 2" -i 5
expect_stderr "comment is required" "missing comment message on stderr"
# 5. Value-less flags: rc 2 with "requires a value" on stderr. The old parser
# died here silently (set -e on the failed shift 2).
for flag in -i -b -c -l --issue --body --comment --login; 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 5 -b --help
expect_rc 2 "short flag value rejected" -i 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"
# 6. Alias acceptance at parse level: both -b and -c carry a value past
# parsing; the wrapper then fails at platform detection (not a git repo)
# nonzero but NOT as a usage error (rc must not be 2).
for flag in -b -c; do
rc=0
run_wrapper_sandboxed -i 5 "$flag" "some text" >"$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
# 6b. GitHub-path exit normalization (codex blocker on 08a00149): gh's own
# usage errors exit 2; the wrapper must NOT propagate that status (reserved
# for the wrapper's usage-error contract). With a github remote and a gh stub
# whose comment write exits 2, the wrapper must exit 1 with the normalized
# error on stderr.
GH_REPO="$WORK_DIR/repo-gh"
mkdir -p "$GH_REPO"
git -C "$GH_REPO" init -q
git -C "$GH_REPO" remote add origin https://github.com/acme/widgets.git
git -C "$GH_REPO" config mosaic.gitIdentity ""
rc=0
(
cd "$GH_REPO"
PATH="$BIN_DIR:$PATH" MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \
"$SCRIPT_DIR/issue-comment.sh" -i 5 -b "text" >"$OUT_FILE" 2>"$ERR_FILE"
) || rc=$?
[[ "$rc" -eq 1 ]] || fail "GitHub path: gh exit 2 must normalize to wrapper exit 1 (got $rc)"
grep -q "GitHub comment write failed" "$ERR_FILE" || fail "GitHub path: normalized error missing from stderr"
grep -q "^gh issue comment" "$PROBE_LOG" || fail "GitHub path: gh write was not invoked"
# 7. No provider contact from any usage-error arm (arm 6b's deliberate gh
# invocation is the only permitted entry in the probe log).
if grep -v '^gh issue comment' "$PROBE_LOG" | grep -q .; then
echo "FAIL: a parser-failure arm contacted a provider:" >&2
grep -v '^gh issue comment' "$PROBE_LOG" >&2
exit 1
fi
echo "issue-comment.sh usage-contract regression passed (R1/R4)"