#!/usr/bin/env bash # Load-bearing regression harness for pr-create.sh identity-first principal # resolution (mosaicstack/stack #1280). # # The failure this harness is written down to catch: `MOSAIC_GIT_IDENTITY=fargo # pr-create.sh …` produces a PR attributed to `mos-dt-0` (whichever account the # tea login list happens to hold). Before #1280 the identity-aware code existed # but sat on the API arm that only ran when the tea path FAILED — tea succeeded, # so the identity arm never executed, and every test that did not check ORDERING # passed. This harness checks ordering directly: # # 1. identity set + slot present -> the PR is created via the REST API with # the identity's per-slot token (asserted by sentinel value AT the fake # provider), and tea's `pr create` is NEVER invoked. # 2. identity set + slot ABSENT -> nonzero, stderr naming the identity and # the expected slot path; neither tea `pr create` nor any API request # fires. No silent fallback to the tea login list. # 3. identity set + --login -> --login wins: tea runs WITH the explicit # --login, no API request. # 4. nothing set -> preserved behavior: tea path with the tea-list login. # # Uses a stubbed tea, a stubbed curl provider, stubbed credentials.json and # per-slot token under a fake HOME. NEVER reads real secrets or hits a live # forge — all assertions are against the stubs' logs. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-create-identity-first}" FAKE_HOME="$WORK_DIR/home" REPO_DIR="$WORK_DIR/repo" TOOLS_DIR="$WORK_DIR/tools" BIN_DIR="$WORK_DIR/bin" LOG_FILE="$WORK_DIR/calls.log" CREDENTIALS_FILE="$FAKE_HOME/.config/mosaic/credentials.json" rm -rf "$WORK_DIR" mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" "$FAKE_HOME/.config/tea" \ "$REPO_DIR" "$TOOLS_DIR/git" "$TOOLS_DIR/_lib" "$BIN_DIR" # Fixture: the real scripts under test, copied so sibling stubs (and the # ../_lib credential loader) resolve inside the fixture tree. cp "$SCRIPT_DIR/pr-create.sh" "$TOOLS_DIR/git/pr-create.sh" cp "$SCRIPT_DIR/detect-platform.sh" "$TOOLS_DIR/git/detect-platform.sh" cp "$SCRIPT_DIR/../_lib/credentials.sh" "$TOOLS_DIR/_lib/credentials.sh" chmod +x "$TOOLS_DIR/git/pr-create.sh" git -C "$REPO_DIR" init -q git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git cat > "$CREDENTIALS_FILE" <<'JSON' { "gitea": { "mosaicstack": { "url": "https://git.mosaicstack.dev", "token": "shared-mosaicstack-token" } } } JSON cat > "$FAKE_HOME/.config/tea/config.yml" <<'YAML' logins: - name: alice url: https://git.mosaicstack.dev token: SECRET-alice-tea-token YAML echo -n "SECRET-agentX-slot-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentX.token" : > "$LOG_FILE" # Stubbed tea: records every invocation; `login list` feeds login resolution; # `api --login /user` feeds get_gitea_authenticated_user; `pr create` marks # the marker file (its presence fails the identity-mode assertions). cat > "$BIN_DIR/tea" <> "$LOG_FILE" if [[ "\$*" == "login list --output json" ]]; then cat <<'JSON' [ {"name":"alice","url":"https://git.mosaicstack.dev","default":true} ] JSON exit 0 fi if [[ "\${1:-}" == "api" ]]; then printf '%s\n' '{"login":"alice"}' exit 0 fi if [[ "\$*" == pr\ create* ]]; then echo "TEA-PR-CREATE-INVOKED" >> "$LOG_FILE" exit 0 fi exit 0 SH chmod +x "$BIN_DIR/tea" # Stubbed provider: records the URL and the Authorization header VALUE it # received, answers 201 with a created-PR object. The sentinel token values are # synthetic fixtures — asserting them at the provider proves WHICH slot's # credential carried the write. cat > "$BIN_DIR/curl" <> "$LOG_FILE" cat <<'JSON' {"number": 1299, "html_url": "https://git.mosaicstack.dev/mosaicstack/stack/pulls/1299"} JSON exit 0 SH chmod +x "$BIN_DIR/curl" fail=0 assert_contains() { local desc="$1" needle="$2" if ! grep -qF -- "$needle" "$LOG_FILE"; then echo "FAIL: $desc — log does not contain '$needle':" >&2 cat "$LOG_FILE" >&2 fail=1 fi } assert_not_contains() { local desc="$1" needle="$2" if grep -qF -- "$needle" "$LOG_FILE"; then echo "FAIL: $desc — log must not contain '$needle':" >&2 cat "$LOG_FILE" >&2 fail=1 fi } EXTRA_ARGS="" run_pr_create() { # "$@" carries ONLY environment assignments (VAR=value); EXTRA_ARGS (if # set) carries wrapper arguments, so `env` never mistakes a wrapper flag # like --login for one of its own. ( cd "$REPO_DIR" # shellcheck disable=SC2086 # EXTRA_ARGS is deliberately word-split wrapper args env -i HOME="$FAKE_HOME" PATH="$BIN_DIR:$PATH" \ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \ MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" "$@" \ bash "$TOOLS_DIR/git/pr-create.sh" -t "Test PR" -B next -H fix/test $EXTRA_ARGS ) } # --------------------------------------------------------------------------- # 1. HAPPY PATH (the load-bearing ordering test): identity set + slot present # -> REST API with the per-slot token; tea `pr create` NEVER invoked. # --------------------------------------------------------------------------- set +e out=$(run_pr_create MOSAIC_GIT_IDENTITY=agentX 2>"$WORK_DIR/stderr-1.tmp") rc=$? set -e if [[ "$rc" -ne 0 ]]; then echo "FAIL: identity happy path — expected rc=0, got $rc" >&2 cat "$WORK_DIR/stderr-1.tmp" >&2 fail=1 fi assert_contains "identity happy path reaches the API" "CURL-URL: https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/pulls" assert_contains "identity happy path carries the slot token" "CURL-AUTH: Authorization: token SECRET-agentX-slot-token" assert_not_contains "identity happy path must NOT invoke tea pr create" "TEA-PR-CREATE-INVOKED" # --------------------------------------------------------------------------- # 2. Identity set + slot ABSENT -> fail loud BEFORE any write: nonzero, stderr # naming identity + slot path, no tea pr create, no API request. # --------------------------------------------------------------------------- : > "$LOG_FILE" set +e out=$(run_pr_create MOSAIC_GIT_IDENTITY=agentNoSlot 2>"$WORK_DIR/stderr-2.tmp") rc=$? set -e if [[ "$rc" -eq 0 ]]; then echo "FAIL: missing slot — expected nonzero return, got 0 (stdout='$out')" >&2 fail=1 fi err=$(cat "$WORK_DIR/stderr-2.tmp") if [[ "$err" != *"agentNoSlot"* ]]; then echo "FAIL: missing slot — stderr does not name the identity:" >&2 echo "$err" >&2 fail=1 fi if [[ "$err" != *"$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentNoSlot.token"* ]]; then echo "FAIL: missing slot — stderr does not name the expected slot path:" >&2 echo "$err" >&2 fail=1 fi assert_not_contains "missing slot must not reach tea pr create" "TEA-PR-CREATE-INVOKED" assert_not_contains "missing slot must not reach the API" "CURL-URL" # --------------------------------------------------------------------------- # 3. Identity set + --login -> --login wins: tea runs WITH the explicit login. # --------------------------------------------------------------------------- : > "$LOG_FILE" EXTRA_ARGS="--login alice" set +e out=$(run_pr_create MOSAIC_GIT_IDENTITY=agentX 2>"$WORK_DIR/stderr-3.tmp") rc=$? set -e EXTRA_ARGS="" if [[ "$rc" -ne 0 ]]; then echo "FAIL: login override — expected rc=0, got $rc" >&2 cat "$WORK_DIR/stderr-3.tmp" >&2 fail=1 fi assert_contains "login override drives tea with the explicit login" "TEA: pr create --repo mosaicstack/stack --login alice" assert_not_contains "login override must not hit the API" "CURL-URL" # --------------------------------------------------------------------------- # 4. Nothing set -> preserved behavior: tea path with the tea-list login. # --------------------------------------------------------------------------- : > "$LOG_FILE" set +e out=$(run_pr_create 2>"$WORK_DIR/stderr-4.tmp") rc=$? set -e if [[ "$rc" -ne 0 ]]; then echo "FAIL: default path — expected rc=0, got $rc" >&2 cat "$WORK_DIR/stderr-4.tmp" >&2 fail=1 fi assert_contains "default path still uses the tea-list login" "TEA: pr create --repo mosaicstack/stack --login alice" if [[ "$fail" -eq 0 ]]; then echo "pr-create identity-first happy-path regression passed" fi exit "$fail"