#!/usr/bin/env bash # Regression harness for `git-credential-mosaic` — per-agent Gitea identity # resolution (Gate-16 author≠reviewer separation). # # Covers: # 1. Identity resolution priority: MOSAIC_GIT_IDENTITY env > git config # mosaic.gitIdentity (per-worktree) > git-supplied username. # 2. Correct per-slot token file path chosen per host # (gitea-usc-.token vs gitea-mosaicstack-.token). # 3. Per-slot token present -> emits that identity + token. # 4. Per-slot token absent -> falls back to the shared account # (backward-compat / no-op for hosts without per-slot tokens). # 5. Unknown/unrelated host -> exits 0 with no output (passthrough). # # Uses stubbed token files under a fake HOME + a real (throwaway) git repo. # NEVER reads real secrets or touches the real ~/.config/mosaic/secrets. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/git-credential-mosaic}" FAKE_HOME="$WORK_DIR/home" REPO_DIR="$WORK_DIR/repo" # Mirror the real deployed layout (~/.config/mosaic/tools/{git,_lib}/) under the # fake HOME: git-credential-mosaic resolves its credentials.sh sibling via a # script-relative path (BASH_SOURCE), so the copy must live next to a stubbed # _lib/credentials.sh, not the real one, to keep this test hermetic. HELPER="$FAKE_HOME/.config/mosaic/tools/git/git-credential-mosaic" rm -rf "$WORK_DIR" mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" \ "$FAKE_HOME/.config/mosaic/tools/git" \ "$FAKE_HOME/.config/mosaic/tools/_lib" \ "$REPO_DIR" cp "$SCRIPT_DIR/git-credential-mosaic" "$HELPER" chmod +x "$HELPER" git -C "$REPO_DIR" init -q git -C "$REPO_DIR" config user.email "test@example.invalid" git -C "$REPO_DIR" config user.name "Test" # Fake shared-account credential loader — stands in for # tools/_lib/credentials.sh's load_credentials(), scoped to this test only. cat > "$FAKE_HOME/.config/mosaic/tools/_lib/credentials.sh" <<'SH' load_credentials() { case "$1" in gitea-mosaicstack) GITEA_URL="https://git.mosaicstack.dev"; GITEA_TOKEN="shared-mosaicstack-token"; export GITEA_URL GITEA_TOKEN; return 0 ;; gitea-usc) GITEA_URL="https://git.uscllc.com"; GITEA_TOKEN="shared-usc-token"; export GITEA_URL GITEA_TOKEN; return 0 ;; *) return 1 ;; esac } SH fail=0 assert_eq() { local desc="$1" expected="$2" actual="$3" if [[ "$expected" != "$actual" ]]; then echo "FAIL: $desc — expected '$expected', got '$actual'" >&2 fail=1 fi } # Feed "host=\nusername=\n\n" on stdin (mirrors git's credential protocol) # and run the helper with the fake HOME, inside REPO_DIR (so `git config # mosaic.gitIdentity` resolves per-worktree), plus any extra env passed in $@. run_helper() { local host="$1" username_in="$2"; shift 2 ( cd "$REPO_DIR" env -i HOME="$FAKE_HOME" PATH="$PATH" "$@" bash "$HELPER" get < shared fallback # (backward-compat: unchanged behavior when nothing is configured). # --------------------------------------------------------------------------- git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true out=$(run_helper "git.mosaicstack.dev" "") assert_eq "shared fallback: username" "username=git" "$(echo "$out" | grep '^username=')" assert_eq "shared fallback: password" "password=shared-mosaicstack-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 2. git-supplied username resolves to an identity WITH a per-slot token -> # that identity + token wins over the shared account. # --------------------------------------------------------------------------- echo -n "agentA-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentA.token" out=$(run_helper "git.mosaicstack.dev" "agentA") assert_eq "username-resolved identity: username" "username=agentA" "$(echo "$out" | grep '^username=')" assert_eq "username-resolved identity: password" "password=agentA-mosaicstack-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 3. git config mosaic.gitIdentity (per-worktree) beats git-supplied username. # --------------------------------------------------------------------------- echo -n "agentB-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentB.token" git -C "$REPO_DIR" config mosaic.gitIdentity agentB out=$(run_helper "git.mosaicstack.dev" "agentA") assert_eq "git-config beats username: username" "username=agentB" "$(echo "$out" | grep '^username=')" assert_eq "git-config beats username: password" "password=agentB-mosaicstack-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 4. MOSAIC_GIT_IDENTITY env beats git config mosaic.gitIdentity. # --------------------------------------------------------------------------- echo -n "agentC-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentC.token" out=$(run_helper "git.mosaicstack.dev" "agentA" MOSAIC_GIT_IDENTITY=agentC) assert_eq "env beats git-config: username" "username=agentC" "$(echo "$out" | grep '^username=')" assert_eq "env beats git-config: password" "password=agentC-mosaicstack-token" "$(echo "$out" | grep '^password=')" git -C "$REPO_DIR" config --unset mosaic.gitIdentity # --------------------------------------------------------------------------- # 5. Identity resolves, but no matching per-slot token file -> falls back to # the shared account (per-agent identity is opt-in, not a hard requirement). # --------------------------------------------------------------------------- out=$(run_helper "git.mosaicstack.dev" "no-such-agent") assert_eq "no per-slot token: username" "username=git" "$(echo "$out" | grep '^username=')" assert_eq "no per-slot token: password" "password=shared-mosaicstack-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 6. Correct per-slot token PATH is chosen per host: same agent id, different # host prefix (gitea-usc- vs gitea-mosaicstack-). # --------------------------------------------------------------------------- echo -n "agentD-usc-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-usc-agentD.token" out=$(run_helper "git.uscllc.com" "agentD") assert_eq "host-scoped token path (usc): username" "username=agentD" "$(echo "$out" | grep '^username=')" assert_eq "host-scoped token path (usc): password" "password=agentD-usc-token" "$(echo "$out" | grep '^password=')" # agentD has NO mosaicstack token -> must fall back to shared mosaicstack, not # leak the usc token across hosts. out=$(run_helper "git.mosaicstack.dev" "agentD") assert_eq "host-scoped token path (cross-host must not leak): username" "username=git" "$(echo "$out" | grep '^username=')" assert_eq "host-scoped token path (cross-host must not leak): password" "password=shared-mosaicstack-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 7. Unrelated/unknown host -> exit 0, no output (passthrough for non-Gitea # remotes, e.g. github.com via a different credential helper). # --------------------------------------------------------------------------- out=$(run_helper "github.com" "agentA") assert_eq "unknown host: no output" "" "$out" # --------------------------------------------------------------------------- # 8. Non-"get" verb (store/erase) -> exit 0, no output (git-credential # protocol: this helper only implements get). # --------------------------------------------------------------------------- store_out=$(cd "$REPO_DIR" && env -i HOME="$FAKE_HOME" PATH="$PATH" bash "$HELPER" store <