From e09b8783b4ca6532b834acf138a3aed65a513a77 Mon Sep 17 00:00:00 2001 From: marcie Date: Sat, 29 Aug 2026 21:03:07 +0000 Subject: [PATCH] P1b: read-only viewers join the R1/R4 usage contract (#1472) Co-authored-by: marcie --- .../mosaic/framework/tools/git/pr-diff.sh | 20 ++- .../mosaic/framework/tools/git/pr-list.sh | 17 ++- .../mosaic/framework/tools/git/pr-metadata.sh | 18 ++- .../mosaic/framework/tools/git/pr-view.sh | 18 ++- .../tools/git/test-pr-diff-usage-contract.sh | 132 ++++++++++++++++++ .../tools/git/test-pr-list-usage-contract.sh | 131 +++++++++++++++++ .../git/test-pr-metadata-usage-contract.sh | 132 ++++++++++++++++++ .../tools/git/test-pr-view-usage-contract.sh | 132 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 9 files changed, 586 insertions(+), 16 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-pr-diff-usage-contract.sh create mode 100755 packages/mosaic/framework/tools/git/test-pr-list-usage-contract.sh create mode 100755 packages/mosaic/framework/tools/git/test-pr-metadata-usage-contract.sh create mode 100755 packages/mosaic/framework/tools/git/test-pr-view-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/pr-diff.sh b/packages/mosaic/framework/tools/git/pr-diff.sh index 4657b5de..a09d1f78 100755 --- a/packages/mosaic/framework/tools/git/pr-diff.sh +++ b/packages/mosaic/framework/tools/git/pr-diff.sh @@ -13,21 +13,33 @@ OUTPUT_FILE="" REPO_OVERRIDE="" HOST_OVERRIDE="" +# Usage-error contract (R4): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + echo "Usage: pr-diff.sh -n [-r owner/repo] [--host host] [-o ] (see --help)" >&2 + exit 2 +} + while [[ $# -gt 0 ]]; do case $1 in -n|--number) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" PR_NUMBER="$2" shift 2 ;; -o|--output) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" OUTPUT_FILE="$2" shift 2 ;; -r|--repo) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" REPO_OVERRIDE="$2" shift 2 ;; --host) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" HOST_OVERRIDE="$2" shift 2 ;; @@ -40,18 +52,18 @@ while [[ $# -gt 0 ]]; do echo " --host Gitea host for --repo API calls (or set GITEA_HOST/GITEA_URL)" echo " -o, --output Output file (optional, prints to stdout if omitted)" echo " -h, --help Show this help" + echo "" + echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential failure." exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$PR_NUMBER" ]]; then - echo "Error: PR number is required (-n)" >&2 - exit 1 + usage_error "PR number is required (-n/--number)" fi if [[ -n "$REPO_OVERRIDE" ]]; then diff --git a/packages/mosaic/framework/tools/git/pr-list.sh b/packages/mosaic/framework/tools/git/pr-list.sh index 11eebeab..dd75453a 100755 --- a/packages/mosaic/framework/tools/git/pr-list.sh +++ b/packages/mosaic/framework/tools/git/pr-list.sh @@ -34,29 +34,41 @@ Examples: $(basename "$0") -s merged -a username # Merged PRs by user $(basename "$0") --repo ddk/ai-bma # List PRs from anywhere EOF - exit "${1:-1}" + exit "${1:-2}" } # Parse arguments +# Usage-error contract (R4): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + usage >&2 +} + while [[ $# -gt 0 ]]; do case $1 in -s|--state) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" STATE="$2" shift 2 ;; -l|--label) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LABEL="$2" shift 2 ;; -a|--author) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" AUTHOR="$2" shift 2 ;; -n|--limit) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LIMIT="$2" shift 2 ;; -r|--repo) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" REPO_OVERRIDE="$2" shift 2 ;; @@ -64,8 +76,7 @@ while [[ $# -gt 0 ]]; do usage 0 ;; *) - echo "Unknown option: $1" >&2 - usage + usage_error "unknown option: $1" ;; esac done diff --git a/packages/mosaic/framework/tools/git/pr-metadata.sh b/packages/mosaic/framework/tools/git/pr-metadata.sh index e625cfda..6089efc6 100755 --- a/packages/mosaic/framework/tools/git/pr-metadata.sh +++ b/packages/mosaic/framework/tools/git/pr-metadata.sh @@ -12,13 +12,23 @@ source "$SCRIPT_DIR/detect-platform.sh" PR_NUMBER="" OUTPUT_FILE="" +# Usage-error contract (R4): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + echo "Usage: pr-metadata.sh -n [-o ] (see --help)" >&2 + exit 2 +} + while [[ $# -gt 0 ]]; do case $1 in -n|--number) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" PR_NUMBER="$2" shift 2 ;; -o|--output) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" OUTPUT_FILE="$2" shift 2 ;; @@ -29,18 +39,18 @@ while [[ $# -gt 0 ]]; do echo " -n, --number PR number (required)" echo " -o, --output Output file (optional, prints to stdout if omitted)" echo " -h, --help Show this help" + echo "" + echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential failure." exit 0 ;; *) - echo "Unknown option: $1" >&2 - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$PR_NUMBER" ]]; then - echo "Error: PR number is required (-n)" >&2 - exit 1 + usage_error "PR number is required (-n/--number)" fi write_metadata() { diff --git a/packages/mosaic/framework/tools/git/pr-view.sh b/packages/mosaic/framework/tools/git/pr-view.sh index 72f53074..484d5ff1 100755 --- a/packages/mosaic/framework/tools/git/pr-view.sh +++ b/packages/mosaic/framework/tools/git/pr-view.sh @@ -11,13 +11,23 @@ source "$SCRIPT_DIR/detect-platform.sh" PR_NUMBER="" REPO_OVERRIDE="" +# Usage-error contract (R4): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + echo "Usage: pr-view.sh -n [-r owner/repo] (see --help)" >&2 + exit 2 +} + while [[ $# -gt 0 ]]; do case $1 in -n|--number) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" PR_NUMBER="$2" shift 2 ;; -r|--repo) + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" REPO_OVERRIDE="$2" shift 2 ;; @@ -28,18 +38,18 @@ while [[ $# -gt 0 ]]; do echo " -n, --number PR number (required)" echo " -r, --repo Repository slug (default: infer from git origin)" echo " -h, --help Show this help" + echo "" + echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential failure." exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$PR_NUMBER" ]]; then - echo "Error: PR number is required (-n)" - exit 1 + usage_error "PR number is required (-n/--number)" fi if [[ -n "$REPO_OVERRIDE" ]]; then diff --git a/packages/mosaic/framework/tools/git/test-pr-diff-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-diff-usage-contract.sh new file mode 100755 index 00000000..58d1d08c --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-diff-usage-contract.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# Usage-error contract for pr-diff.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/pr-diff-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-diff.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-diff.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-diff.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 -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 -o -r --number --output --repo --host; 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 --help +expect_rc 2 "short flag value rejected" -n -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 -n; do + rc=0 + run_wrapper_sandboxed -n 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 "pr-diff.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/framework/tools/git/test-pr-list-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-list-usage-contract.sh new file mode 100755 index 00000000..c4ed38c9 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-list-usage-contract.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# Usage-error contract for pr-list.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/pr-list-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-list.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-list.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-list.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. + + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -s -l -a -n -r --state --label --author --limit --repo; 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" -s --help +expect_rc 2 "short flag value rejected" -s -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 -s; do + rc=0 + run_wrapper_sandboxed -s open >"$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-list.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/framework/tools/git/test-pr-metadata-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-metadata-usage-contract.sh new file mode 100755 index 00000000..9c735aec --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-metadata-usage-contract.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# Usage-error contract for pr-metadata.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/pr-metadata-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-metadata.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-metadata.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-metadata.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 -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 -o --number --output; 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 --help +expect_rc 2 "short flag value rejected" -n -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 -n; do + rc=0 + run_wrapper_sandboxed -n 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 "pr-metadata.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/framework/tools/git/test-pr-view-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-view-usage-contract.sh new file mode 100755 index 00000000..66ea3b4a --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-view-usage-contract.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# Usage-error contract for pr-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/pr-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/pr-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/pr-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: pr-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 -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 -r --number --repo; 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 --help +expect_rc 2 "short flag value rejected" -n -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 -n; do + rc=0 + run_wrapper_sandboxed -n 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 "pr-view.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index de8f4569..70d121a9 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-baseline.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-issue-assign-usage-contract.sh && bash framework/tools/git/test-milestone-close-usage-contract.sh && bash framework/tools/git/test-milestone-list-usage-contract.sh && bash framework/tools/git/test-issue-view-usage-contract.sh && bash framework/tools/git/test-issue-list-usage-contract.sh && bash framework/tools/git/test-milestone-create-usage-contract.sh && bash framework/tools/git/test-lane-brief-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-baseline.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-pr-diff-usage-contract.sh && bash framework/tools/git/test-pr-view-usage-contract.sh && bash framework/tools/git/test-pr-metadata-usage-contract.sh && bash framework/tools/git/test-pr-list-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-issue-assign-usage-contract.sh && bash framework/tools/git/test-milestone-close-usage-contract.sh && bash framework/tools/git/test-milestone-list-usage-contract.sh && bash framework/tools/git/test-issue-view-usage-contract.sh && bash framework/tools/git/test-issue-list-usage-contract.sh && bash framework/tools/git/test-milestone-create-usage-contract.sh && bash framework/tools/git/test-lane-brief-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*",