#!/usr/bin/env bash
# Fixture test for deckctl sync (workstation pull/apply/diff, fail-closed secrets).
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
DECKCTL="$ROOT/bin/deckctl"
FIX=$(mktemp -d "${TMPDIR:-/tmp}/deckctl-sync-test-XXXXXX")
trap 'rm -rf "$FIX"' EXIT
pass=0; fail=0
ok()  { echo "  PASS: $1"; pass=$((pass+1)); }
bad() { echo "  FAIL: $1"; fail=$((fail+1)); }

export TEST_HOME="$FIX/home"
mkdir -p "$TEST_HOME/.local/state/overdeck/backups"
cp "$ROOT/modules/workstation/claude/buildbox-hosts.json" "$FIX/buildbox-hosts.json"

# deckctl resolves its root from bin/ location, so the suite drives a sandbox copy:
# writing into the real modules/workstation/ would clobber committed workstation config.
SANDBOX="$FIX/deploy"
mkdir -p "$SANDBOX/modules"
cp -a "$ROOT/bin" "$ROOT/lib" "$SANDBOX/"
cp -a "$ROOT/modules/workstation" "$SANDBOX/modules/"
cp -a "$ROOT/modules/fleet" "$SANDBOX/modules/"
mkdir -p "$SANDBOX/modules/buildbox"
cp "$ROOT/modules/buildbox/devtools.json" "$SANDBOX/modules/buildbox/"
rm -rf "$SANDBOX/modules/workstation"/*/
mkdir -p "$SANDBOX/modules/workstation/manifest" "$SANDBOX/modules/workstation/claude/lib"
cp -a "$ROOT/modules/workstation/claude/lib/." "$SANDBOX/modules/workstation/claude/lib/"
cp -a "$ROOT/modules/workstation/manifest/." "$SANDBOX/modules/workstation/manifest/"
cp -a "$ROOT/modules/workstation/bin" "$SANDBOX/modules/workstation/"
mkdir -p "$SANDBOX/modules/buildbox/bin"
cp "$ROOT/modules/buildbox/bin/buildbox-scratch-bind" "$SANDBOX/modules/buildbox/bin/"
REPO_WS="$SANDBOX/modules/workstation"

DEPLOY_WS="$REPO_WS"
mkdir -p "$DEPLOY_WS/claude"/{bin,lib,hooks}

pin_deploy() {
  git -C "$SANDBOX" add -A
  git -C "$SANDBOX" diff --cached --quiet \
    || git -C "$SANDBOX" commit -qm "update fixture"
  git -C "$SANDBOX" update-ref refs/remotes/origin/main HEAD
  git -C "$SANDBOX" rev-parse --verify HEAD >"$SANDBOX/.git/deploy-pinned-sha"
}

run_deckctl() {
  pin_deploy
  HOME="$TEST_HOME" OVERDECK_DEPLOY_DIR="$SANDBOX" BUILDBOX_HOSTS_CONFIG="$FIX/buildbox-hosts.json" \
    "$SANDBOX/bin/deckctl" "$@"
}

echo "=== prereq: sync.sh and workstation module exist ==="
for f in "$ROOT/lib/deckctl/sync.sh" "$ROOT/modules/workstation/deck.module.json" "$ROOT/modules/workstation/deny.list"; do
  [[ -f "$f" ]] && ok "$(basename "$f") exists" || bad "missing $f"
done
[[ -d "$ROOT/modules/workstation/manifest" ]] && ok "manifest dir exists" || bad "missing manifest dir"
for agent in claude codex cursor-agent opencode gemini kiro pi antigravity; do
  [[ -f "$ROOT/modules/workstation/manifest/${agent}.json" ]] && ok "manifest $agent" || bad "missing manifest $agent"
done

echo "=== bash -n clean ==="
bash -n "$ROOT/lib/deckctl/sync.sh" && ok "bash -n sync.sh" || bad "bash -n sync.sh"

git -C "$SANDBOX" init -q
git -C "$SANDBOX" config user.name Fixture
git -C "$SANDBOX" config user.email fixture@example.invalid
pin_deploy

set +e
out_bin_first=$(run_deckctl sync apply bin 2>&1)
rc_bin_first=$?
out_bin_second=$(run_deckctl sync apply bin 2>&1)
rc_bin_second=$?
set -e
bin_link="$TEST_HOME/.local/bin/buildbox-scratch-bind"
bin_target="$SANDBOX/modules/workstation/bin/buildbox-scratch-bind"
if [[ $rc_bin_first -eq 0 && $rc_bin_second -eq 0 && -L "$bin_link" \
  && "$(readlink "$bin_link")" == "$bin_target" && -x "$bin_link" ]]; then
  ok "repeated bin apply retains deploy-sourced buildbox-scratch-bind"
else
  bad "repeated bin apply failed: first rc=$rc_bin_first output=$out_bin_first; second rc=$rc_bin_second output=$out_bin_second"
fi

make_deploy_repo() {
  local repo="$1"
  mkdir -p "$repo"
  git -C "$repo" init -q
  git -C "$repo" config user.name Fixture
  git -C "$repo" config user.email fixture@example.invalid
  printf 'fixture\n' >"$repo/fixture"
  git -C "$repo" add fixture
  git -C "$repo" commit -qm "fixture"
  git -C "$repo" update-ref refs/remotes/origin/main HEAD
  git -C "$repo" rev-parse --verify HEAD >"$repo/.git/deploy-pinned-sha"
}

run_provenance() {
  local root="$1" deploy="$2"
  DECKCTL_ROOT="$root" OVERDECK_DEPLOY_DIR="$deploy" \
    bash -c 'die() { printf "%s\n" "$1" >&2; exit 1; }; source "$1"; sync_assert_deploy_provenance claude bin' \
      _ "$ROOT/lib/deckctl/sync.sh"
}

echo "=== deploy provenance rejects every mutable or ambiguous source ==="
PROVENANCE="$FIX/provenance"
make_deploy_repo "$PROVENANCE"
if run_provenance "$PROVENANCE" "$PROVENANCE"; then
  ok "pristine standalone deploy clone at origin/main is accepted"
else
  bad "pristine standalone deploy clone was rejected"
fi
set +e
out=$(run_provenance "$FIX" "$PROVENANCE" 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && grep -q 'may only be installed by deckctl from the deploy clone' <<<"$out"; then
  ok "deckctl outside the deploy clone is rejected"
else
  bad "wrong deckctl root rc=$rc output=$out"
fi
printf 'dirty\n' >"$PROVENANCE/dirty"
FAKE_BIN="$FIX/fake-bin"
mkdir -p "$FAKE_BIN"
printf '#!/usr/bin/env bash\n[[ " $* " == *" status "* ]] && exit 0\nexec /usr/bin/git "$@"\n' >"$FAKE_BIN/git"
chmod +x "$FAKE_BIN/git"
set +e
out=$(PATH="$FAKE_BIN:$PATH" run_provenance "$PROVENANCE" "$PROVENANCE" 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && grep -q 'deploy clone is dirty' <<<"$out"; then
  ok "dirty deploy clone is rejected using trusted Git"
else
  bad "dirty deploy clone rc=$rc output=$out"
fi
rm "$PROVENANCE/dirty"
printf 'new\n' >>"$PROVENANCE/fixture"
git -C "$PROVENANCE" add fixture
git -C "$PROVENANCE" commit -qm "ahead"
set +e
out=$(run_provenance "$PROVENANCE" "$PROVENANCE" 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && grep -q 'not pinned to its recorded checkout' <<<"$out"; then
  ok "deploy clone ahead of its recorded pin is rejected"
else
  bad "unpinned deploy clone rc=$rc output=$out"
fi
WORKTREE_BASE="$FIX/worktree-base"
WORKTREE_DEPLOY="$FIX/worktree-deploy"
make_deploy_repo "$WORKTREE_BASE"
git -C "$WORKTREE_BASE" worktree add -q --detach "$WORKTREE_DEPLOY" HEAD
set +e
out=$(run_provenance "$WORKTREE_DEPLOY" "$WORKTREE_DEPLOY" 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && grep -q 'standalone git checkout' <<<"$out"; then
  ok "linked worktree deploy source is rejected"
else
  bad "worktree deploy source rc=$rc output=$out"
fi

echo "=== selected registry apply survives regular-file replacement ==="
cp "$FIX/buildbox-hosts.json" "$DEPLOY_WS/claude/buildbox-hosts.json"
mkdir -p "$TEST_HOME/.claude"
printf '{"stale":true}\n' >"$TEST_HOME/.claude/buildbox-hosts.json"
pin_deploy
set +e
out=$(env -u BUILDBOX_HOSTS_CONFIG HOME="$TEST_HOME" OVERDECK_DEPLOY_DIR="$SANDBOX" \
  "$SANDBOX/bin/deckctl" sync apply claude buildbox-hosts.json 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 && -L "$TEST_HOME/.claude/buildbox-hosts.json" ]]; then
  ok "registry apply converges after backing up regular destination"
else
  bad "registry apply rc=$rc output=$out"
fi

CLAUDE_HOME="$TEST_HOME/.claude"
mkdir -p "$CLAUDE_HOME/skills/demo"
printf 'skill-body\n' >"$CLAUDE_HOME/skills/demo/SKILL.md"
printf '{"theme":"dark"}\n' >"$CLAUDE_HOME/settings.json"

set +e
out=$(run_deckctl sync pull claude 2>&1)
rc_pull=$?
out_apply=$(run_deckctl sync apply claude 2>&1)
rc_apply=$?
out_diff=$(run_deckctl sync diff claude 2>&1)
rc_diff=$?
set -e

repo_skills="$REPO_WS/claude/skills/demo/SKILL.md"
repo_settings="$REPO_WS/claude/settings.json"

if [[ $rc_pull -eq 0 ]]; then ok "pull exits 0"; else bad "pull rc=$rc_pull: $out"; fi
if [[ -f "$repo_skills" ]]; then ok "pull copied skills tree to repo"; else bad "repo skills missing"; fi
if [[ -f "$repo_settings" ]]; then ok "pull copied settings.json to repo"; else bad "repo settings missing"; fi
if [[ $rc_apply -eq 0 ]]; then ok "apply exits 0"; else bad "apply rc=$rc_apply: $out_apply"; fi
if [[ -L "$CLAUDE_HOME/skills" ]]; then
  target=$(readlink "$CLAUDE_HOME/skills")
  [[ "$target" == "$REPO_WS/claude/skills" ]] && ok "apply linked skills symlink" || bad "skills symlink -> $target"
else
  bad "skills not symlink after apply"
fi
if [[ $rc_diff -eq 0 ]]; then ok "diff clean after round-trip"; else bad "diff rc=$rc_diff: $out_diff"; fi

echo "=== pull excludes planted credentials.json (deny-list) ==="
rm -f "$CLAUDE_HOME/skills"
mkdir -p "$CLAUDE_HOME/skills/demo"
printf 'skill-body\n' >"$CLAUDE_HOME/skills/demo/SKILL.md"
printf 'secret\n' >"$CLAUDE_HOME/skills/credentials.json"
printf 'noise\n' >"$CLAUDE_HOME/skills/demo/run.log"
mkdir -p "$CLAUDE_HOME/skills/scratchpad/checkout"
printf 'scratch\n' >"$CLAUDE_HOME/skills/scratchpad/checkout/file.txt"
set +e
out=$(run_deckctl sync pull claude 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]]; then ok "pull exits 0 with deny-listed files present"; else bad "pull rc=$rc: $out"; fi
if [[ ! -e "$REPO_WS/claude/skills/credentials.json" ]]; then ok "credentials.json not copied"; else bad "credentials.json reached repo"; fi
if [[ ! -e "$REPO_WS/claude/skills/demo/run.log" ]]; then ok "run.log not copied"; else bad "run.log reached repo"; fi
if [[ ! -e "$REPO_WS/claude/skills/scratchpad" ]]; then ok "scratchpad tree not copied"; else bad "scratchpad reached repo"; fi
if [[ -f "$REPO_WS/claude/skills/demo/SKILL.md" ]]; then ok "non-denied sibling still copied"; else bad "SKILL.md missing after prune"; fi
if printf '%s\n' "$out" | grep -q 'excluded deny-listed /credentials.json'; then ok "pull reports the exclusion"; else bad "pull output missing exclusion line: $out"; fi
rm -rf "$CLAUDE_HOME/skills/credentials.json" "$CLAUDE_HOME/skills/demo/run.log" "$CLAUDE_HOME/skills/scratchpad"

echo "=== refused pull leaves the agent tree unchanged ==="
rm -rf "$CLAUDE_HOME/skills"; mkdir -p "$CLAUDE_HOME/skills/demo"
printf 'good-body\n' >"$CLAUDE_HOME/skills/demo/SKILL.md"
run_deckctl sync pull claude >/dev/null 2>&1
# assembled at runtime so no literal secret-shaped string is stored in this repo
printf 'stripe_key = "sk_%s_%s"\n' 'live' "$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 24)" >"$CLAUDE_HOME/CLAUDE.md"
set +e
out=$(run_deckctl sync pull claude 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]]; then ok "pull refuses planted secret"; else bad "pull accepted planted secret"; fi
if [[ ! -e "$REPO_WS/claude/CLAUDE.md" ]]; then ok "rejected entry not left in repo"; else bad "rejected CLAUDE.md left in repo"; fi
if [[ "$(cat "$REPO_WS/claude/skills/demo/SKILL.md" 2>/dev/null)" == "good-body" ]]; then ok "prior agent tree restored intact"; else bad "agent tree lost after refused pull"; fi
rm -f "$CLAUDE_HOME/CLAUDE.md"

echo "=== pull excludes non-portable symlinks ==="
rm -rf "$CLAUDE_HOME/skills"; mkdir -p "$CLAUDE_HOME/skills/demo"
printf 'skill-body\n' >"$CLAUDE_HOME/skills/demo/SKILL.md"
ln -s /nonexistent/gone "$CLAUDE_HOME/skills/dangling"
mkdir -p "$FIX/elsewhere"; printf 'other\n' >"$FIX/elsewhere/foreign.md"
ln -s "$FIX/elsewhere/foreign.md" "$CLAUDE_HOME/skills/escaping"
ln -s demo/SKILL.md "$CLAUDE_HOME/skills/inside"
set +e
out=$(run_deckctl sync pull claude 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]]; then ok "pull exits 0 with non-portable symlinks"; else bad "pull rc=$rc: $out"; fi
if [[ ! -e "$REPO_WS/claude/skills/dangling" && ! -L "$REPO_WS/claude/skills/dangling" ]]; then ok "dangling symlink not copied"; else bad "dangling symlink reached repo"; fi
if [[ ! -L "$REPO_WS/claude/skills/escaping" ]]; then ok "escaping symlink not copied"; else bad "escaping symlink reached repo"; fi
if [[ -L "$REPO_WS/claude/skills/inside" ]]; then ok "in-tree symlink preserved"; else bad "in-tree symlink dropped"; fi
if printf '%s\n' "$out" | grep -q 'excluded non-portable symlink /dangling'; then ok "pull reports non-portable symlink"; else bad "output missing symlink line: $out"; fi
rm -f "$CLAUDE_HOME/skills/dangling" "$CLAUDE_HOME/skills/escaping" "$CLAUDE_HOME/skills/inside"

echo "=== apply backs up foreign file before symlink ==="
rm -rf "$CLAUDE_HOME/skills"
run_deckctl sync pull claude >/dev/null 2>&1 || true
printf 'foreign-tree\n' >"$CLAUDE_HOME/skills"
set +e
out=$(run_deckctl sync apply claude 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]]; then ok "apply over foreign file exits 0"; else bad "apply rc=$rc: $out"; fi
if [[ -L "$CLAUDE_HOME/skills" ]]; then ok "foreign file replaced with symlink"; else bad "skills not symlink after backup apply"; fi
backup_root="$TEST_HOME/.local/state/overdeck/backups"
if find "$backup_root" -type f -name skills 2>/dev/null | grep -q .; then
  ok "foreign skills backed up under state/backups"
else
  bad "no backup found for foreign skills"
fi

echo "=== diff reports drift ==="
printf 'drift\n' >>"$CLAUDE_HOME/settings.json"
set +e
out=$(run_deckctl sync diff claude 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]]; then ok "diff exits 1 on drift"; else bad "diff rc=$rc on drift"; fi
if printf '%s\n' "$out" | grep -q 'settings.json'; then ok "diff names drifted entry"; else bad "diff output: $out"; fi

echo "=== selected apply changes only one manifest entry ==="
mkdir -p "$REPO_WS/claude/scripts"
printf '#!/usr/bin/env bash\n' >"$REPO_WS/claude/scripts/manifest-entry"
rm -rf "$CLAUDE_HOME/scripts"
printf 'keep-local-settings\n' >"$CLAUDE_HOME/settings.json"
set +e
out=$(run_deckctl sync apply claude scripts 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]]; then ok "selected apply exits 0"; else bad "selected apply rc=$rc: $out"; fi
if [[ -L "$CLAUDE_HOME/scripts" && "$(readlink "$CLAUDE_HOME/scripts")" == "$REPO_WS/claude/scripts" ]]; then
  ok "selected apply links requested entry"
else
  bad "selected apply did not link scripts"
fi
if [[ ! -L "$CLAUDE_HOME/settings.json" && "$(cat "$CLAUDE_HOME/settings.json")" == "keep-local-settings" ]]; then
  ok "selected apply preserves unrelated entries"
else
  bad "selected apply changed settings.json"
fi
set +e
out=$(run_deckctl sync apply claude missing-entry 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'unknown manifest entry'; then
  ok "selected apply rejects unknown entry"
else
  bad "selected apply accepted unknown entry: rc=$rc output=$out"
fi

echo "=== deploy-sourced entries resolve to the deploy clone ==="
printf '#!/usr/bin/env bash\n' >"$DEPLOY_WS/claude/bin/local-gate"
cp "$ROOT/modules/workstation/claude/buildbox-hosts.json" "$DEPLOY_WS/claude/buildbox-hosts.json"
rm -rf "$CLAUDE_HOME/bin"
set +e
out=$(run_deckctl sync apply claude bin 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 && "$(readlink "$CLAUDE_HOME/bin")" == "$DEPLOY_WS/claude/bin" ]]; then
  ok "apply links deploy-sourced entry to the deploy clone"
else
  bad "deploy-sourced apply rc=$rc target=$(readlink "$CLAUDE_HOME/bin" 2>&1)"
fi
set +e
out=$(run_deckctl sync apply claude buildbox-hosts.json 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 && "$(readlink "$CLAUDE_HOME/buildbox-hosts.json")" == "$DEPLOY_WS/claude/buildbox-hosts.json" ]]; then
  ok "apply links buildbox registry to the deploy clone"
else
  bad "deploy-sourced registry apply rc=$rc target=$(readlink "$CLAUDE_HOME/buildbox-hosts.json" 2>&1)"
fi
set +e
out=$(run_deckctl sync diff claude 2>&1)
rc=$?
set -e
if printf '%s\n' "$out" | grep -q 'claude:bin'; then
  bad "diff reports drift on a correctly deploy-linked entry: $out"
else
  ok "diff accepts the deploy target"
fi
set +e
out=$(run_deckctl sync pull claude 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]] && printf '%s\n' "$out" | grep -q 'deploy-sourced'; then
  ok "pull skips deploy-sourced entries with a reason"
else
  bad "pull on deploy-sourced entry rc=$rc output=$out"
fi
if [[ ! -e "$REPO_WS/claude/bin/.pulled" && -f "$REPO_WS/claude/bin/local-gate" ]]; then
  ok "pull leaves deploy-sourced bin untouched"
else
  bad "pull changed deploy-sourced bin"
fi
set +e
out=$(HOME="$TEST_HOME" OVERDECK_DEPLOY_DIR="$FIX/no-such-deploy" "$SANDBOX/bin/deckctl" sync apply claude bin 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'deploy clone missing'; then
  ok "missing deploy clone fails closed"
else
  bad "missing deploy clone rc=$rc output=$out"
fi

echo "=== apply refuses to overwrite drifted copy entry ==="
printf '{"theme":"dark"}\n' >"$CLAUDE_HOME/settings.json"
run_deckctl sync pull claude >/dev/null 2>&1
printf '{"theme":"dark","live":"edit"}\n' >"$CLAUDE_HOME/settings.json"
printf '{"theme":"repo"}\n' >"$REPO_WS/claude/settings.json"
set +e
out=$(run_deckctl sync apply claude settings.json 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'live edits'; then ok "apply refuses drifted settings.json"; else bad "drifted apply rc=$rc output=$out"; fi
if [[ "$(cat "$CLAUDE_HOME/settings.json")" == '{"theme":"dark","live":"edit"}' ]]; then ok "live file untouched after refusal"; else bad "live settings clobbered"; fi

echo "=== pull then apply clears the drift refusal ==="
set +e
out=$(run_deckctl sync pull claude 2>&1)
rc_pull=$?
out_apply=$(run_deckctl sync apply claude settings.json 2>&1)
rc_apply=$?
set -e
if [[ $rc_pull -eq 0 && $rc_apply -eq 0 ]]; then ok "pull+apply succeed after drift"; else bad "pull rc=$rc_pull apply rc=$rc_apply: $out $out_apply"; fi
if [[ "$(cat "$CLAUDE_HOME/settings.json")" == '{"theme":"dark","live":"edit"}' ]]; then ok "pull made repo match live"; else bad "settings content after pull+apply: $(cat "$CLAUDE_HOME/settings.json")"; fi

echo "=== apply overwrites when live matches baseline ==="
printf '{"theme":"repo-v2"}\n' >"$REPO_WS/claude/settings.json"
set +e
out=$(run_deckctl sync apply claude settings.json 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 && "$(cat "$CLAUDE_HOME/settings.json")" == '{"theme":"repo-v2"}' ]]; then ok "unchanged live updated from repo"; else bad "repo-forward apply rc=$rc content=$(cat "$CLAUDE_HOME/settings.json") output=$out"; fi

echo "=== missing baseline with drift fails closed ==="
rm -rf "$TEST_HOME/.local/state/overdeck/sync-baseline"
printf '{"theme":"local-only"}\n' >"$CLAUDE_HOME/settings.json"
set +e
out=$(run_deckctl sync apply claude settings.json 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'live edits'; then ok "no-baseline drift refused"; else bad "no-baseline drift rc=$rc output=$out"; fi

echo "=== Pi deploy-owned settings round-trip: apply -> runtime edit -> pull -> apply ==="
PI_DEV="$FIX/pi-dev"
PI_DEPLOY="$FIX/pi-deploy"
cp -a "$SANDBOX/." "$PI_DEV/"
cp -a "$SANDBOX/." "$PI_DEPLOY/"
rm -rf "$PI_DEV/.git" "$PI_DEPLOY/.git"
for root in "$PI_DEV" "$PI_DEPLOY"; do
  git -C "$root" init -q
  git -C "$root" config user.name Fixture
  git -C "$root" config user.email fixture@example.invalid
  mkdir -p "$root/modules/workstation/pi/agent"
  cp "$ROOT/modules/workstation/manifest/pi.json" "$root/modules/workstation/manifest/pi.json"
  printf '{"theme":"dark"}\n' >"$root/modules/workstation/pi/agent/settings.json"
  git -C "$root" add -A
  git -C "$root" commit -qm fixture
  git -C "$root" update-ref refs/remotes/origin/main HEAD
  git -C "$root" rev-parse --verify HEAD >"$root/.git/deploy-pinned-sha"
done
PI_HOME="$TEST_HOME/.pi"
rm -rf "$PI_HOME"
set +e
out_apply=$(HOME="$TEST_HOME" OVERDECK_DEPLOY_DIR="$PI_DEPLOY" BUILDBOX_HOSTS_CONFIG="$FIX/buildbox-hosts.json" \
  "$PI_DEPLOY/bin/deckctl" sync apply pi 2>&1)
rc_apply=$?
set -e
if [[ $rc_apply -eq 0 && -L "$PI_HOME/agent/settings.json" && "$(readlink "$PI_HOME/agent/settings.json")" == "$PI_DEPLOY/modules/workstation/pi/agent/settings.json" ]]; then
  ok "Pi apply links settings only to canonical deploy"
else
  bad "Pi canonical apply rc=$rc_apply target=$(readlink "$PI_HOME/agent/settings.json" 2>&1) output=$out_apply"
fi
printf '{"theme":"dark","runtime":"edit"}\n' >"$PI_HOME/agent/settings.json"
set +e
out_pull=$(HOME="$TEST_HOME" OVERDECK_DEPLOY_DIR="$PI_DEPLOY" BUILDBOX_HOSTS_CONFIG="$FIX/buildbox-hosts.json" \
  "$PI_DEV/bin/deckctl" sync pull pi 2>&1)
rc_pull=$?
set -e
if [[ $rc_pull -eq 0 && "$(cat "$PI_DEV/modules/workstation/pi/agent/settings.json")" == '{"theme":"dark","runtime":"edit"}' ]]; then
  ok "Pi pull captures canonical runtime edit into repository transaction"
else
  bad "Pi runtime pull rc=$rc_pull content=$(cat "$PI_DEV/modules/workstation/pi/agent/settings.json" 2>&1) output=$out_pull"
fi
cp "$PI_DEV/modules/workstation/pi/agent/settings.json" "$PI_DEPLOY/modules/workstation/pi/agent/settings.json"
git -C "$PI_DEPLOY" add modules/workstation/pi/agent/settings.json
git -C "$PI_DEPLOY" commit -qm "land runtime setting"
git -C "$PI_DEPLOY" update-ref refs/remotes/origin/main HEAD
git -C "$PI_DEPLOY" rev-parse --verify HEAD >"$PI_DEPLOY/.git/deploy-pinned-sha"
set +e
out_apply=$(HOME="$TEST_HOME" OVERDECK_DEPLOY_DIR="$PI_DEPLOY" BUILDBOX_HOSTS_CONFIG="$FIX/buildbox-hosts.json" \
  "$PI_DEPLOY/bin/deckctl" sync apply pi 2>&1)
rc_apply=$?
out_diff=$(HOME="$TEST_HOME" OVERDECK_DEPLOY_DIR="$PI_DEPLOY" BUILDBOX_HOSTS_CONFIG="$FIX/buildbox-hosts.json" \
  "$PI_DEPLOY/bin/deckctl" sync diff pi 2>&1)
rc_diff=$?
set -e
if [[ $rc_apply -eq 0 && $rc_diff -eq 0 ]]; then
  ok "Pi apply after landed pull has zero drift"
else
  bad "Pi final apply rc=$rc_apply diff rc=$rc_diff output=$out_apply $out_diff"
fi
ln -sfn "$PI_DEV/modules/workstation/pi/agent/settings.json" "$PI_HOME/agent/settings.json"
set +e
out=$(HOME="$TEST_HOME" OVERDECK_DEPLOY_DIR="$PI_DEPLOY" BUILDBOX_HOSTS_CONFIG="$FIX/buildbox-hosts.json" \
  "$PI_DEV/bin/deckctl" sync pull pi 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && grep -q 'foreign path' <<<"$out"; then
  ok "Pi pull rejects foreign settings writer"
else
  bad "Pi foreign writer accepted rc=$rc output=$out"
fi

echo "=== Pi wrapper persists settings through deckctl transaction ==="
PI_WRAPPER_HOME="$FIX/pi-wrapper-home"
PI_WRAPPER_DEPLOY="$FIX/pi-wrapper-deploy"
mkdir -p "$PI_WRAPPER_HOME/.local/bin" "$PI_WRAPPER_DEPLOY/bin" "$PI_WRAPPER_DEPLOY/modules/workstation/pi/agent"
cp "$ROOT/modules/workstation/bin/pi" "$PI_WRAPPER_HOME/.local/bin/pi"
printf '#!/usr/bin/env bash\nprintf "{\\"runtime\\":\\"saved\\"}\\n" >"$PI_CODING_AGENT_DIR/settings.json"\nexit 7\n' >"$FIX/real-pi"
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$*" >"%s"\n' "$FIX/deckctl-call" >"$PI_WRAPPER_DEPLOY/bin/deckctl"
chmod +x "$PI_WRAPPER_HOME/.local/bin/pi" "$FIX/real-pi" "$PI_WRAPPER_DEPLOY/bin/deckctl"
set +e
HOME="$PI_WRAPPER_HOME" OVERDECK_DEPLOY_DIR="$PI_WRAPPER_DEPLOY" OVERDECK_REAL_PI="$FIX/real-pi" \
  "$PI_WRAPPER_HOME/.local/bin/pi" --fixture >/dev/null 2>&1
rc=$?
set -e
if [[ $rc -eq 7 && "$(cat "$FIX/deckctl-call")" == "sync pull pi" ]]; then
  ok "Pi wrapper preserves Pi exit and runs one pull transaction"
else
  bad "Pi wrapper rc=$rc deckctl=$(cat "$FIX/deckctl-call" 2>&1)"
fi

echo "=== deploy registry replacement is validated and non-destructive ==="
registry_source="$DEPLOY_WS/claude/buildbox-hosts.json"
registry_live="$CLAUDE_HOME/buildbox-hosts.json"
printf '{"registry":"existing"}\n' >"$FIX/existing-registry.json"
ln -sfn "$FIX/existing-registry.json" "$registry_live"
rm -f "$registry_source"
run_deckctl sync apply claude buildbox-hosts.json >/dev/null
[[ "$(readlink "$registry_live")" == "$FIX/existing-registry.json" ]] && ok "missing candidate preserves valid existing registry" || bad "missing candidate changed existing registry"
printf '{invalid\n' >"$registry_source"
set +e
out=$(run_deckctl sync apply claude buildbox-hosts.json 2>&1); rc=$?
set -e
if [[ $rc -ne 0 && "$(readlink "$registry_live")" == "$FIX/existing-registry.json" ]]; then ok "invalid candidate fails without changing registry"; else bad "invalid candidate rc=$rc target=$(readlink "$registry_live" 2>&1) output=$out"; fi
cp "$ROOT/modules/workstation/claude/buildbox-hosts.json" "$registry_source"
run_deckctl sync apply claude buildbox-hosts.json >/dev/null
if [[ -L "$registry_live" && "$(readlink "$registry_live")" == "$registry_source" ]]; then ok "valid candidate replaces registry atomically"; else bad "valid candidate did not replace registry"; fi
mapfile -t registry_backups < <(find "$TEST_HOME/.local/state/overdeck/backups" -type l -path '*/.claude/buildbox-hosts.json' -print)
backup_link=""
for candidate in "${registry_backups[@]}"; do
  if [[ "$(readlink "$candidate")" == "$FIX/existing-registry.json" ]]; then
    backup_link="$candidate"
    break
  fi
done
if [[ -n "$backup_link" ]]; then ok "successful replacement preserves prior registry backup"; else bad "prior registry backup missing"; fi

echo "=== src override symlinks dest name to a differently-named repo file ==="
# CLAUDE.md installs from claude-home.md so the repo file stays off the cwd-walk
# auto-load path (double-load fix); the manifest src field carries the mapping.
printf 'src-override content\n' > "$REPO_WS/claude/claude-home.md"
set +e
run_deckctl sync apply claude CLAUDE.md >/dev/null 2>&1
rc=$?
set -e
link_target=$(readlink "$TEST_HOME/.claude/CLAUDE.md" 2>/dev/null || true)
if [[ $rc -eq 0 && "$link_target" == */claude/claude-home.md \
  && "$(cat "$TEST_HOME/.claude/CLAUDE.md")" == "src-override content" ]]; then
  ok "src override installs CLAUDE.md from claude-home.md"
else
  bad "src override rc=$rc link=$link_target"
fi

echo "=== unknown agent fails closed ==="
set +e
out=$(run_deckctl sync pull nosuch 2>&1)
rc=$?
set -e
if [[ $rc -ne 0 ]] && printf '%s\n' "$out" | grep -q 'unknown agent'; then
  ok "unknown agent rejected"
else
  bad "unknown agent rc=$rc output=$out"
fi

echo "----"
echo "PASS=$pass FAIL=$fail"
[[ $fail -eq 0 ]]
