#!/bin/bash
# Sandboxed AC verification for act:c008862c (mux half) — the worktree
# "MERGE OR LOSE" false-positive class: mux-manufactured churn (identity
# symlink typechange, node_modules/pib.db symlinks, machine-written
# generated state) must never render as dirt or count as unmerged work,
# while authored files, real edits, and real commits ALWAYS surface.
#
# Mechanism note (verified live 2026-07-13): git reads info/exclude from the
# COMMON git dir only — the per-worktree worktrees/<name>/info/exclude was a
# file git never consulted, and exclude rules can never hide a TRACKED
# file's typechange. The implementation therefore (a) points a per-worktree
# core.excludesFile (extensions.worktreeConfig) at the managed exclude so it
# becomes live, and (b) holds skip-worktree on tracked identity files WHILE
# they are mux symlinks into main, clearing the bit the moment a real file
# returns. The dirty-check carries three independent guards so a deletion
# can never race the health heal: symlink-gated untracked exclusions (5),
# skip-worktree'd non-symlinks count dirty (5b), and hidden real files at
# identity paths count dirty (5c).
#
# Fully sandboxed: throwaway repos under mktemp, HOME + GIT_CONFIG_GLOBAL +
# XDG_CONFIG_HOME overridden, TMUX unset (never touches the operator's
# live window indicators). Dirty-check verdicts are asserted BEFORE the
# first health run wherever possible — the live exclude otherwise shadows
# the dirty-check's own exclusions and a revert would pass green
# (mutation-testing lesson from this lane's CP).
set -uo pipefail
unset TMUX

REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
SANDBOX=$(mktemp -d /tmp/cc-statusclean-test.XXXXXX)
FAKE_HOME="$SANDBOX/home"
PROJ="$SANDBOX/proj"
WT="$SANDBOX/wt"
HEALTH="$REPO_ROOT/templates/mux/config/worktree-session-health.sh"
DIRTY="$REPO_ROOT/templates/mux/config/worktree-dirty-check.sh"

export GIT_CONFIG_GLOBAL="$SANDBOX/gitconfig-global"
export XDG_CONFIG_HOME="$FAKE_HOME/.config"

pass=0; fail=0
ok()  { echo "  PASS: $1"; pass=$((pass+1)); }
bad() { echo "  FAIL: $1"; fail=$((fail+1)); }
assert() { # assert <desc> <cmd...>
  local desc="$1"; shift
  if "$@" >/dev/null 2>&1; then ok "$desc"; else bad "$desc"; fi
}
run_health() { HOME="$FAKE_HOME" bash "$HEALTH" "$PROJ" "$WT" "$@" 2>&1; }
run_dirty()  { HOME="$FAKE_HOME" bash "$DIRTY" "$WT" "$PROJ" 2>/dev/null; }
wt_status()  { git -C "$WT" status --porcelain --untracked-files=all; }

mkdir -p "$FAKE_HOME/.local/share/mux/wt-health" "$FAKE_HOME/.claude/projects" "$XDG_CONFIG_HOME/git"
echo '.DS_Store' > "$XDG_CONFIG_HOME/git/ignore"
trap 'rm -rf "$SANDBOX"' EXIT

# --- fixture main repo: consumer-shaped ---------------------------------------
# Tracked: .mcp.json, .claudeignore, .claude/plans + .claude/cabinet docs
# (cabinet/ is the tracked-holding dir the generated state hides inside).
# NO project .gitignore coverage for .claude/ infra — the consumer case where
# the dead per-worktree exclude left everything visible.
git init -q "$PROJ"
git -C "$PROJ" config user.email t@t.t; git -C "$PROJ" config user.name t
mkdir -p "$PROJ/.claude/plans" "$PROJ/.claude/cabinet" "$PROJ/.claude/skills"
echo '{"mcpServers":{}}' > "$PROJ/.mcp.json"
echo 'node_modules' > "$PROJ/.claudeignore"
echo plan > "$PROJ/.claude/plans/p.md"
echo checkpoint-doc > "$PROJ/.claude/cabinet/checkpoint.md"
echo code > "$PROJ/file.txt"
printf '%s\n' 'node_modules/' > "$PROJ/.gitignore"
git -C "$PROJ" add -A && git -C "$PROJ" commit -qm init
echo skill > "$PROJ/.claude/skills/s.md"   # untracked infra in main
mkdir -p "$PROJ/node_modules"
touch "$PROJ/pib.db"
MAIN_SLUG=$(echo "$PROJ" | sed 's|[/.]|-|g')
mkdir -p "$FAKE_HOME/.claude/projects/$MAIN_SLUG"

git -C "$PROJ" worktree add -q "$WT" -b mux/lane HEAD

# Simulate exactly what create_worktree manufactures (bin/mux :247-252).
for f in .mcp.json .claudeignore; do
  rm -f "$WT/$f"; ln -s "$PROJ/$f" "$WT/$f"
done
ln -s "$PROJ/pib.db" "$WT/pib.db"
ln -s "$PROJ/node_modules" "$WT/node_modules"

echo "== T0: PRE-HEALTH — status shows the churn, but the dirty-check already classifies honestly"
st0=$(git -C "$WT" status --porcelain)
assert "typechange renders before any health run (precondition)" \
  grep -q ' T .mcp.json' <<<"$st0"
echo '{"dismissed":[]}' > "$WT/.claude/cabinet/advisories-state.json"
echo newplan > "$WT/.claude/plans/new-doc.md"
d0=$(run_dirty)
assert "pre-health: authored doc counts, generated state does NOT (uncommitted=1)" \
  grep -q 'uncommitted=1 verdict=dirty' <<<"$d0"
rm -f "$WT/.claude/plans/new-doc.md"
d0b=$(run_dirty)
assert "pre-health: typechange + symlinks + generated state alone read clean" \
  grep -q 'commits=0 uncommitted=0 verdict=clean' <<<"$d0b"
rm -f "$WT/.claude/cabinet/advisories-state.json"

echo "== T1: fresh worktree shows clean git status after creation health run (AC4)"
run_health --refresh >"$SANDBOX/t1.out"; rc=$?
assert "creation-time health run exits 0" test "$rc" -eq 0
assert "git status is EMPTY (no typechange, no symlink churn, no infra)" \
  test -z "$(wt_status)"
sw1=$(git -C "$WT" ls-files -v .mcp.json)
assert "skip-worktree bit held on .mcp.json" grep -q '^S ' <<<"$sw1"
excf=$(git -C "$WT" config --worktree --get core.excludesFile)
assert "per-worktree excludesFile is live (worktreeConfig)" \
  test "$excf" = "$(git -C "$WT" rev-parse --path-format=absolute --git-dir)/info/exclude"
assert "health reports exclude liveness positively (read-back, not attempt)" \
  grep -q 'per-worktree exclude rules live' "$SANDBOX/t1.out"
d1=$(run_dirty)
assert "dirty-check verdict: clean" grep -q 'verdict=clean' <<<"$d1"

echo "== T2: user-global ignore patterns survive the excludesFile displacement"
touch "$WT/.DS_Store"
assert ".DS_Store stays hidden in the worktree" test -z "$(wt_status)"
rm -f "$WT/.DS_Store"

echo "== T3: git add -A stages NOTHING on a churn-only worktree (the ELOOP class)"
git -C "$WT" add -A 2>/dev/null
assert "index unchanged after add -A" \
  test -z "$(git -C "$WT" diff --cached --name-only)"

echo "== T4: authored uncommitted file still surfaces and still counts (AC2, mux side)"
echo newplan > "$WT/.claude/plans/new-doc.md"
st4=$(wt_status)
assert "authored plans doc visible in status" \
  grep -q '.claude/plans/new-doc.md' <<<"$st4"
d4=$(run_dirty)
assert "dirty-check counts the authored doc (uncommitted=1, dirty)" \
  grep -q 'uncommitted=1 verdict=dirty' <<<"$d4"
rm -f "$WT/.claude/plans/new-doc.md"

echo "== T5: generated state inside a tracked-holding dir is churn, not work (AC1, mux side)"
echo '{"dismissed":[]}' > "$WT/.claude/cabinet/advisories-state.json"
echo '{"fires":{}}' > "$WT/.claude/cabinet/checklist-stats.json"
run_health >/dev/null 2>&1
assert "generated state hidden from status (re-exclude beats the cabinet/ negation)" \
  test -z "$(wt_status)"
d5=$(run_dirty)
assert "dirty-check verdict clean with only generated state present" \
  grep -q 'verdict=clean' <<<"$d5"

echo "== T6: commits ahead still produce the dirty verdict (AC3, mux side)"
echo work >> "$WT/file.txt"
git -C "$WT" commit -qam "real work"
d6=$(run_dirty)
assert "dirty-check reports commits=1, verdict=dirty" \
  grep -q 'commits=1 uncommitted=0 verdict=dirty' <<<"$d6"
git -C "$PROJ" merge -q mux/lane --no-edit 2>/dev/null
d6b=$(run_dirty)
assert "after merge to main the verdict returns to clean" \
  grep -q 'verdict=clean' <<<"$d6b"

echo "== T7: a REAL .mcp.json edit is never frozen — bit clears at health, edit surfaces"
rm -f "$WT/.mcp.json"
echo '{"mcpServers":{"new":{}}}' > "$WT/.mcp.json"
run_health >/dev/null 2>&1
sw7=$(git -C "$WT" ls-files -v .mcp.json)
assert "skip-worktree bit CLEARED once the path holds a real file" grep -q '^H ' <<<"$sw7"
st7=$(git -C "$WT" status --porcelain)
assert "the real edit renders in status" grep -q '.mcp.json' <<<"$st7"
d7=$(run_dirty)
assert "dirty-check counts the real .mcp.json edit as work" grep -q 'verdict=dirty' <<<"$d7"
rm -f "$WT/.mcp.json"; ln -s "$PROJ/.mcp.json" "$WT/.mcp.json"
run_health >/dev/null 2>&1

echo "== T7b: mid-session symlink→real swap, NO health run — deletion must not race the heal"
rm -f "$WT/.mcp.json"
echo '{"mcpServers":{"midsession":{}}}' > "$WT/.mcp.json"   # bit still set, porcelain blind
st7b=$(git -C "$WT" status --porcelain)
assert "hazard precondition: porcelain is EMPTY while the real edit hides behind the bit" \
  test -z "$st7b"
d7b=$(run_dirty)
assert "dirty-check STILL reads dirty (skip-worktree'd non-symlink counted, 5b)" \
  grep -q 'verdict=dirty' <<<"$d7b"
rm -f "$WT/.mcp.json"; ln -s "$PROJ/.mcp.json" "$WT/.mcp.json"
run_health >/dev/null 2>&1

echo "== T7c: a foreign-target symlink never earns skip-worktree"
rm -f "$WT/.mcp.json"; ln -s "$SANDBOX/elsewhere.json" "$WT/.mcp.json"
run_health >/dev/null 2>&1
sw7c=$(git -C "$WT" ls-files -v .mcp.json)
assert "foreign symlink: bit NOT held (H, typechange stays visible)" grep -q '^H ' <<<"$sw7c"
st7c=$(git -C "$WT" status --porcelain)
assert "foreign symlink renders as a typechange" grep -q ' T .mcp.json' <<<"$st7c"
rm -f "$WT/.mcp.json"; ln -s "$PROJ/.mcp.json" "$WT/.mcp.json"
run_health >/dev/null 2>&1

echo "== T9: root-anchored patterns — a NESTED authored .mcp.json is never hidden"
mkdir -p "$WT/packages/svc"
echo '{"mcpServers":{"svc":{}}}' > "$WT/packages/svc/.mcp.json"
run_health >/dev/null 2>&1
st9=$(wt_status)
assert "nested .mcp.json visible in status after a health rebuild" \
  grep -q 'packages/svc/.mcp.json' <<<"$st9"
d9=$(run_dirty)
assert "nested .mcp.json counts as work (dirty)" grep -q 'verdict=dirty' <<<"$d9"
rm -rf "$WT/packages"

echo "== T10: a REAL untracked pib.db (symlink replaced) is work, even before the exclude rebuilds"
rm -f "$WT/pib.db"
echo 'sqlite-data' > "$WT/pib.db"      # stale /pib.db exclude line still live
d10=$(run_dirty)
assert "pre-rebuild: hidden real pib.db counts dirty (5c)" grep -q 'verdict=dirty' <<<"$d10"
run_health >/dev/null 2>&1              # rebuild drops the /pib.db line (no symlink)
st10=$(wt_status)
assert "post-rebuild: real pib.db visible in status" grep -q 'pib.db' <<<"$st10"
d10b=$(run_dirty)
assert "post-rebuild: real pib.db still dirty (symlink gate off)" grep -q 'verdict=dirty' <<<"$d10b"
rm -f "$WT/pib.db"; ln -s "$PROJ/pib.db" "$WT/pib.db"
run_health >/dev/null 2>&1

echo "== T8: idempotence — repeat health runs, still clean, no duplicate exclude lines"
run_health >/dev/null 2>&1; run_health >/dev/null 2>&1
assert "status still empty after repeat runs" test -z "$(wt_status)"
ex_file="$(git -C "$WT" rev-parse --path-format=absolute --git-dir)/info/exclude"
assert "managed exclude lines not duplicated" \
  bash -c "[ \"\$(grep -cxF '/pib.db' '$ex_file')\" -eq 1 ] && [ \"\$(grep -cxF '.claude/cabinet/advisories-state.json' '$ex_file')\" -eq 1 ]"
assert "marker blocks not duplicated" \
  bash -c "[ \"\$(grep -c '>>> mux-managed worktree rules' '$ex_file')\" -eq 1 ] && [ \"\$(grep -c '>>> mux-managed user ignore copy' '$ex_file')\" -eq 1 ]"
assert "user ignore copy precedes managed rules (authored negations win)" \
  bash -c "[ \"\$(grep -n 'mux-managed user ignore copy' '$ex_file' | head -1 | cut -d: -f1)\" -lt \"\$(grep -n 'mux-managed worktree rules' '$ex_file' | head -1 | cut -d: -f1)\" ]"

echo "== T11: core.bare guard — a bare-repo layout is never half-migrated to worktreeConfig"
BARE="$SANDBOX/bare.git"; BPROJ="$SANDBOX/bareproj"; BWT="$SANDBOX/barewt"
git init -q --bare "$BARE"
git clone -q "$BARE" "$SANDBOX/seed" 2>/dev/null
( cd "$SANDBOX/seed" && git config user.email t@t.t && git config user.name t \
  && mkdir -p .claude/plans && echo p > .claude/plans/p.md && echo x > f.txt \
  && git add -A && git commit -qm init && git push -q origin HEAD:main )
git -C "$BARE" worktree add -q "$BWT" main 2>/dev/null
mkdir -p "$BPROJ/.claude"   # fake proj dir so the health flow reaches the rebuild
HOME="$FAKE_HOME" bash "$HEALTH" "$BPROJ" "$BWT" >"$SANDBOX/t11.out" 2>&1 || true
wtc=$(git -C "$BWT" config --get extensions.worktreeConfig 2>/dev/null) || wtc=""
assert "extensions.worktreeConfig NOT enabled on a bare shared config" test -z "$wtc"
assert "bare guard reported visibly, not silently" \
  grep -q 'core.bare=true' "$SANDBOX/t11.out"
assert "git status still works in the bare repo's worktree" \
  git -C "$BWT" status --porcelain

echo ""
echo "RESULT: ${pass} passed, ${fail} failed"
[[ "$fail" -eq 0 ]]
