MOSAIC_GIT_IDENTITY=fargo produced objects attributed to mos-dt-0: every write wrapper resolved its acting principal from tea's login list, which enumerates whatever logins the host happens to hold and knows nothing about which seat is calling. The identity-aware code was present and correct but unreachable on the happy path — it sat on arms that only ran when tea failed. One shared resolver, not twenty patches: resolve_gitea_principal() in detect-platform.sh implements the precedence (explicit --login beats MOSAIC_GIT_IDENTITY / worktree git config mosaic.gitIdentity; the tea login list is the LAST resort), fails loud (nonzero, naming the identity or login and the expected slot path) when the requested principal has no credential, and never prints a token value. gitea_identity_token_slot() is the single source of truth for the slot layout, shared with get_gitea_token, so resolver and token resolution cannot disagree. Call-site conversions (the proving five): pr-review.sh (principal resolved once for every action; the comment action now honors --login), issue-comment.sh, pr-create.sh (identity mode reaches the REST API on the HAPPY path — tea is never consulted, so the login list cannot shadow the identity; --login wins even on the tea-failure fallback arm), issue-create.sh (same), pr-merge.sh (gains --login; --dry-run reports the principal the merge WOULD act as, resolved exactly as the merge resolves it; no cross-principal fallback — an identity-bound 401 is a hard stop). Remaining wrappers are call-site conversions onto the same resolver, measured: write-path issue-assign, issue-close, issue-edit, issue-reopen, milestone-close, milestone-create, pr-close; read-path issue-list, milestone-list, pr-list, pr-view (issue-view mixed). pr-diff, pr-metadata, pr-ci-wait and ci-queue-wait already inherit identity-first resolution via get_gitea_token. Known interaction: on a host with a workstation-GLOBAL mosaic.gitIdentity, this fix activates identity mode for every seat that has not set a local one — correct behavior driven by a wrong configuration (measured: #1282-#1287, six accidental live issues, closed with provenance by fred). Set mosaic.gitIdentity per-worktree, never --global. Tests: test-gitea-principal-resolution.sh (resolver matrix — identity present/absent, --login precedence, env vs git-config, unrecognized-host containment, slot-path-by-path-never-by-value); test-pr-create-identity- first.sh (the load-bearing ordering test: identity arm REACHED on the happy path with tea never invoked, fail-loud BEFORE any write on a missing slot, --login wins, default preserved); test-pr-merge-principal- resolution.sh (dry-run truthfulness, merge credential binding, unknown --login never reaches the provider). All wired into test:framework-shell. Sabotage control: precedence inverted to tea-list-first inside the resolver -> exactly the three new suites redden with the #1280 signatures (identity resolves to the tea-list account; missing slot returns rc=0 with silent fallthrough) while all 11 pre-existing git suites stay green; restored byte-identical (sha256 verified); all 14 green again.
245 lines
8.6 KiB
Bash
Executable File
245 lines
8.6 KiB
Bash
Executable File
#!/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 <n> /user` feeds get_gitea_authenticated_user; `pr create` marks
|
|
# the marker file (its presence fails the identity-mode assertions).
|
|
cat > "$BIN_DIR/tea" <<SH
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
printf 'TEA: %s\n' "\$*" >> "$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" <<SH
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
url=""
|
|
auth=""
|
|
while [[ \$# -gt 0 ]]; do
|
|
case "\$1" in
|
|
-H)
|
|
case "\$2" in
|
|
Authorization*) auth="\$2" ;;
|
|
esac
|
|
shift 2
|
|
;;
|
|
*) [[ -n "\$1" && "\$1" != -* ]] && url="\$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
printf 'CURL-URL: %s\nCURL-AUTH: %s\n' "\$url" "\$auth" >> "$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"
|