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.
248 lines
8.3 KiB
Bash
Executable File
248 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Regression harness for pr-merge.sh identity-first principal resolution
|
|
# (mosaicstack/stack #1280).
|
|
#
|
|
# Covers:
|
|
# 1. --dry-run reports the acting principal the merge WOULD use, resolved the
|
|
# same way the real merge resolves it: --login > MOSAIC_GIT_IDENTITY /
|
|
# git config mosaic.gitIdentity > shared host credential. (The pre-#1280
|
|
# deployed copy reported a tea login that the merge would not act as.)
|
|
# 2. --dry-run fails closed when the requested principal has no credential:
|
|
# unknown --login, or an identity with no per-slot token (stderr names
|
|
# the login / the identity and its slot path).
|
|
# 3. The real merge POST carries the resolved principal's credential and no
|
|
# other: --login merges with that login's tea-config token; an identity
|
|
# merges with the per-slot token; an unresolvable --login never reaches
|
|
# the provider.
|
|
#
|
|
# Fixture pattern from test-pr-merge-head-pin.sh: the scripts under test are
|
|
# copied into a fixture tree with stubbed pr-metadata.sh / ci-queue-wait.sh
|
|
# siblings; the provider is a stubbed curl that records the credential it
|
|
# received. NEVER reads real secrets or hits a live forge.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-merge-principal-resolution}"
|
|
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"
|
|
SHA=0123456789abcdef0123456789abcdef01234567
|
|
|
|
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"
|
|
|
|
cp "$SCRIPT_DIR/pr-merge.sh" "$TOOLS_DIR/git/pr-merge.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-merge.sh"
|
|
|
|
git -C "$REPO_DIR" init -q
|
|
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
|
|
# Stubbed siblings pr-merge.sh resolves relative to its own SCRIPT_DIR.
|
|
cat > "$TOOLS_DIR/git/pr-metadata.sh" <<SH
|
|
#!/usr/bin/env bash
|
|
printf '%s\n' '{"baseRefName":"next","headRefName":"fix/pinned","headRefOid":"$SHA","headRepository":"mosaicstack/stack","title":"Test PR","author":{"login":"contributor"}}'
|
|
SH
|
|
cat > "$TOOLS_DIR/git/ci-queue-wait.sh" <<'SH'
|
|
#!/usr/bin/env bash
|
|
exit 0
|
|
SH
|
|
chmod +x "$TOOLS_DIR/git/pr-metadata.sh" "$TOOLS_DIR/git/ci-queue-wait.sh"
|
|
|
|
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: fred-ms
|
|
url: https://git.mosaicstack.dev
|
|
token: SECRET-fred-ms-tea-token
|
|
YAML
|
|
|
|
echo -n "SECRET-agentX-slot-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentX.token"
|
|
|
|
: > "$LOG_FILE"
|
|
|
|
# Stubbed tea for login-list resolution only.
|
|
cat > "$BIN_DIR/tea" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$*" == "login list --output json" ]]; then
|
|
cat <<'JSON'
|
|
[
|
|
{"name":"fred-ms","url":"https://git.mosaicstack.dev","default":true}
|
|
]
|
|
JSON
|
|
exit 0
|
|
fi
|
|
exit 0
|
|
SH
|
|
chmod +x "$BIN_DIR/tea"
|
|
|
|
# Stubbed provider. pr-merge passes curl config on STDIN with -K -; the stub
|
|
# reads stdin, records the Authorization header it received, answers 200.
|
|
cat > "$BIN_DIR/curl" <<SH
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
url=""
|
|
out_file=""
|
|
stdin_config=""
|
|
if [[ ! -t 0 ]]; then
|
|
stdin_config="\$(cat || true)"
|
|
fi
|
|
while [[ \$# -gt 0 ]]; do
|
|
case "\$1" in
|
|
-o) out_file="\$2"; shift 2 ;;
|
|
-K|-w|--max-filesize|--max-time|--connect-timeout|-sS) shift 2 ;;
|
|
*) [[ -n "\$1" && "\$1" != -* && -z "\$url" ]] && url="\$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
auth="\$(printf '%s' "\$stdin_config" | grep -o 'Authorization: token [^"]*' || true)"
|
|
printf 'CURL-URL: %s\nCURL-AUTH: %s\n' "\$url" "\$auth" >> "$LOG_FILE"
|
|
[[ -n "\$out_file" ]] && printf '{}' > "\$out_file"
|
|
printf '200\n'
|
|
exit 0
|
|
SH
|
|
chmod +x "$BIN_DIR/curl"
|
|
|
|
fail=0
|
|
assert_contains_log() {
|
|
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_log() {
|
|
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
|
|
}
|
|
|
|
run_pr_merge() {
|
|
local extra_args="$1"; shift
|
|
(
|
|
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-merge.sh" -n 42 $extra_args
|
|
)
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. --dry-run reports the resolved acting principal truthfully.
|
|
# ---------------------------------------------------------------------------
|
|
out=$(run_pr_merge "--dry-run" MOSAIC_GIT_IDENTITY=agentX)
|
|
if [[ "$out" != *"as git identity 'agentX' (per-slot credential)"* ]]; then
|
|
echo "FAIL: dry-run identity — principal not reported: $out" >&2
|
|
fail=1
|
|
fi
|
|
|
|
out=$(run_pr_merge "--dry-run --login fred-ms" MOSAIC_GIT_IDENTITY=agentX)
|
|
if [[ "$out" != *"as tea login 'fred-ms'"* ]]; then
|
|
echo "FAIL: dry-run login override — login not reported (must beat env identity): $out" >&2
|
|
fail=1
|
|
fi
|
|
|
|
out=$(run_pr_merge "--dry-run")
|
|
if [[ "$out" != *"as default host credential"* ]]; then
|
|
echo "FAIL: dry-run default — not reported: $out" >&2
|
|
fail=1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. --dry-run fails closed when the requested principal has no credential.
|
|
# ---------------------------------------------------------------------------
|
|
stderr_file="$WORK_DIR/stderr.tmp"
|
|
set +e
|
|
out=$(run_pr_merge "--dry-run --login ghost" 2>"$stderr_file")
|
|
rc=$?
|
|
set -e
|
|
if [[ "$rc" -eq 0 ]] || [[ "$(cat "$stderr_file")" != *"ghost"* ]]; then
|
|
echo "FAIL: dry-run unknown --login — expected fail-loud naming 'ghost', rc=$rc" >&2
|
|
cat "$stderr_file" >&2
|
|
fail=1
|
|
fi
|
|
|
|
: > "$stderr_file"
|
|
set +e
|
|
out=$(run_pr_merge "--dry-run" MOSAIC_GIT_IDENTITY=agentNoSlot 2>"$stderr_file")
|
|
rc=$?
|
|
set -e
|
|
err=$(cat "$stderr_file")
|
|
if [[ "$rc" -eq 0 ]] || [[ "$err" != *"agentNoSlot"* ]] \
|
|
|| [[ "$err" != *"$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentNoSlot.token"* ]]; then
|
|
echo "FAIL: dry-run identity without slot — expected fail-loud naming identity + slot path, rc=$rc" >&2
|
|
echo "$err" >&2
|
|
fail=1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. The real merge POST carries the resolved principal's credential ONLY.
|
|
# ---------------------------------------------------------------------------
|
|
: > "$LOG_FILE"
|
|
set +e
|
|
out=$(run_pr_merge "--login fred-ms" MOSAIC_GIT_IDENTITY=agentX 2>"$stderr_file")
|
|
rc=$?
|
|
set -e
|
|
if [[ "$rc" -ne 0 ]]; then
|
|
echo "FAIL: merge with --login — expected rc=0, got $rc" >&2
|
|
cat "$stderr_file" >&2
|
|
fail=1
|
|
fi
|
|
assert_contains_log "merge --login uses the login token" "CURL-AUTH: Authorization: token SECRET-fred-ms-tea-token"
|
|
assert_not_contains_log "merge --login must not use the identity slot token" "SECRET-agentX-slot-token"
|
|
assert_not_contains_log "merge --login must not use the shared token" "shared-mosaicstack-token"
|
|
|
|
: > "$LOG_FILE"
|
|
set +e
|
|
out=$(run_pr_merge "" MOSAIC_GIT_IDENTITY=agentX 2>"$stderr_file")
|
|
rc=$?
|
|
set -e
|
|
if [[ "$rc" -ne 0 ]]; then
|
|
echo "FAIL: merge with identity — expected rc=0, got $rc" >&2
|
|
cat "$stderr_file" >&2
|
|
fail=1
|
|
fi
|
|
assert_contains_log "merge identity uses the per-slot token" "CURL-AUTH: Authorization: token SECRET-agentX-slot-token"
|
|
assert_not_contains_log "merge identity must not use the shared token" "shared-mosaicstack-token"
|
|
|
|
: > "$LOG_FILE"
|
|
set +e
|
|
out=$(run_pr_merge "--login ghost" 2>"$stderr_file")
|
|
rc=$?
|
|
set -e
|
|
if [[ "$rc" -eq 0 ]]; then
|
|
echo "FAIL: merge with unknown --login — expected nonzero, got 0" >&2
|
|
fail=1
|
|
fi
|
|
assert_not_contains_log "merge with unknown --login must not reach the provider" "CURL-URL"
|
|
|
|
if [[ "$fail" -eq 0 ]]; then
|
|
echo "pr-merge identity-first principal resolution regression passed"
|
|
fi
|
|
|
|
exit "$fail"
|