#!/usr/bin/env bash # Self-test for git-ops worktree provisioning. Offline + deterministic (git only, # no network). Covers new-lane.sh (in-repo default, gitignore precondition, # --sibling, main-anchoring, validate-before-mutate, arg hygiene) and the # worktree-guard.sh `git clean` double-force detection. # Resolves paths relative to itself so it runs in the repo and once installed. # # Usage: bash tests/run.sh # Exit: 0 all pass, 1 one or more failures (SKIP+exit 0 if git is unavailable) set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL="$(dirname "$HERE")" NL="$SKILL/scripts/new-lane.sh" # worktree-guard.sh lives in the repo's hooks/ — resolve for both repo + installed layouts. GUARD="" for c in "$SKILL/../../hooks/worktree-guard.sh" "$HOME/.claude/hooks/worktree-guard.sh"; do [ -f "$c" ] && { GUARD="$c"; break; } done command -v git >/dev/null 2>&1 || { echo "SKIP: git not available"; exit 0; } SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT P=0; F=0 ok(){ P=$((P+1)); printf ' PASS %s\n' "$1"; } no(){ F=$((F+1)); printf ' FAIL %s\n' "$1"; } mkrepo(){ rm -rf "$1"; git init -q -b main "$1"; git -C "$1" config user.email t@t git -C "$1" config user.name t; git -C "$1" config core.autocrlf false echo base > "$1/f"; git -C "$1" add -A; git -C "$1" commit -qm init; } echo "=== git-ops new-lane / worktree-guard self-test ===" echo "-- new-lane: in-repo default + gitignore precondition --" mkrepo "$SB/A"; cd "$SB/A" OUT="$(bash "$NL" auth-refactor 2>/dev/null)"; rc=$? [ $rc -eq 0 ] && ok "exit 0" || no "exit $rc" case "$OUT" in */.claude/worktrees/auth-refactor) ok "stdout = in-repo path only";; *) no "stdout=[$OUT]";; esac [ -d "$SB/A/.claude/worktrees/auth-refactor" ] && ok "lane dir created in-repo" || no "lane dir missing" git check-ignore -q .claude/worktrees/.x && ok ".claude/worktrees/ now gitignored" || no "gitignore not ensured" git show-ref --verify --quiet refs/heads/lane/auth-refactor && ok "branch lane/auth-refactor" || no "branch missing" echo "-- new-lane: no duplicate gitignore on second lane --" before=$(grep -c worktrees .gitignore); bash "$NL" hotfix main >/dev/null 2>&1 [ "$before" = "$(grep -c worktrees .gitignore)" ] && ok "gitignore not duplicated" || no "gitignore duplicated" echo "-- new-lane: blanket .claude/ ignore is respected (no append) --" mkrepo "$SB/B"; cd "$SB/B"; printf '.claude/\n' > .gitignore; git add .gitignore; git commit -qm ig sz=$(wc -c <.gitignore); bash "$NL" x >/dev/null 2>&1 [ "$sz" = "$(wc -c <.gitignore)" ] && ok "blanket .claude/ -> gitignore untouched" || no "appended despite blanket ignore" echo "-- new-lane: --sibling places outside the repo --" mkrepo "$SB/S"; cd "$SB/S" OS="$(bash "$NL" --sibling big 2>/dev/null)" [ "$OS" = "$SB/S-big" ] && ok "sibling path = $OS" || no "sibling=[$OS]" [ -d "$SB/S-big" ] && ok "sibling dir outside repo" || no "sibling dir missing" echo "-- new-lane: anchors at MAIN when invoked from inside a lane (no nesting) --" mkrepo "$SB/N"; cd "$SB/N"; L1="$(bash "$NL" first 2>/dev/null)" ( cd "$L1" && bash "$NL" second >/dev/null 2>&1 ) [ -d "$SB/N/.claude/worktrees/second" ] && ok "lane anchored to MAIN" || no "did not anchor to MAIN" [ ! -d "$L1/.claude/worktrees/second" ] && ok "no nested worktree inside the lane" || no "nested worktree leaked" echo "-- new-lane: validate-before-mutate + arg hygiene --" mkrepo "$SB/V"; cd "$SB/V" bash "$NL" lane1 no-such-branch >/dev/null 2>/dev/null; ee=$? [ $ee -eq 2 ] && ok "invalid base -> exit 2" || no "invalid base exit $ee" [ ! -f .gitignore ] && ok "invalid base left no .gitignore" || no "partial gitignore on bad base" bash "$NL" --frobnicate slug >/dev/null 2>/dev/null; ee=$? [ $ee -eq 2 ] && ok "unknown flag -> exit 2" || no "unknown flag exit $ee" [ -z "$(git branch --list 'lane/*')" ] && ok "unknown flag created no branch" || no "stray branch from bad flag" mkrepo "$SB/V2"; cd "$SB/V2"; git branch lane/dup bash "$NL" dup >/dev/null 2>/dev/null; ee=$? [ $ee -eq 1 ] && ok "existing branch -> exit 1" || no "dup exit $ee" [ ! -f .gitignore ] && ok "conflict left no .gitignore" || no "partial gitignore on conflict" bash "$NL" >/dev/null 2>/dev/null; [ $? -eq 2 ] && ok "no slug -> exit 2" || no "empty not exit 2" echo "-- new-lane: slug sanitised; syntax clean --" mkrepo "$SB/Z"; cd "$SB/Z" bash "$NL" "Feat Foo/Bar" >/dev/null 2>&1 git show-ref --verify --quiet refs/heads/lane/feat-foobar && ok "slug 'Feat Foo/Bar' -> lane/feat-foobar" || no "slug sanitise wrong: $(git branch --list 'lane/*')" bash -n "$NL" && ok "new-lane.sh syntax OK" || no "new-lane.sh syntax error" if [ -n "$GUARD" ]; then echo "-- worktree-guard: git clean force-level detection --" bash -n "$GUARD" && ok "worktree-guard.sh syntax OK" || no "guard syntax error" WG="$SB/WG"; mkrepo "$WG"; mkdir -p "$WG/.claude/worktrees" gclean(){ local o; o=$(printf '{"tool_input":{"command":"%s"},"cwd":"%s"}' "$1" "$WG" | bash "$GUARD" 2>&1) if [ "$2" = warn ]; then case "$o" in *double-force*) ok "guard WARN: $1";; *) no "guard expected WARN: $1 [${o:-silent}]";; esac else [ -z "$o" ] && ok "guard silent: $1" || no "guard expected silent: $1"; fi; } gclean "git clean -fdx" silent gclean "git clean -ffdx" warn gclean "git clean --force --force" warn gclean "git clean -f -f" warn gclean "git clean -fd -fx" warn gclean "git clean --force -f" warn gclean "git clean -n" silent gclean "git clean -fd ./somepath" silent # exemption: a session whose cwd is inside a worktree is not warned ex=$(printf '{"tool_input":{"command":"git clean -ffdx"},"cwd":"%s/.claude/worktrees/foo"}' "$WG" | bash "$GUARD" 2>&1) [ -z "$ex" ] && ok "guard exempt inside own worktree" || no "guard warned inside own worktree" # existing rules unregressed for c in "git add -A" "rm -rf .claude/worktrees/foo" "git worktree remove .claude/worktrees/foo"; do o=$(printf '{"tool_input":{"command":"%s"},"cwd":"%s"}' "$c" "$WG" | bash "$GUARD" 2>&1) [ -n "$o" ] && ok "guard still warns: $c" || no "guard regressed: $c" done else echo " SKIP worktree-guard.sh not found (hooks/ unavailable in this layout)" fi echo "-- land-all: classification of mixed branches --" LA="$SKILL/scripts/land-all.sh" bash -n "$LA" && ok "land-all.sh syntax OK" || no "land-all.sh syntax error" R="$SB/LA"; mkrepo "$R"; cd "$R" # LANDABLE: ahead, clean, no worktree, recent commit git checkout -q -b feat/land-me; echo x > x; git add -A; git commit -qm land; git checkout -q main # MERGED: merged into trunk git checkout -q -b feat/merged; echo m > m; git add -A; git commit -qm m; git checkout -q main git merge -q --no-ff -m "merge: feat/merged" feat/merged # NOTHING-AHEAD: points at trunk → ancestor → folds into MERGED git branch feat/at-trunk main # STALE: ahead but last commit older than --recent-days (fixed past ISO date; # relative dates like "30 days ago" aren't accepted by GIT_COMMITTER_DATE on all gits) git checkout -q -b feat/stale; echo s > s; git add -A GIT_AUTHOR_DATE="2020-01-01T00:00:00" GIT_COMMITTER_DATE="2020-01-01T00:00:00" git commit -qm stale git checkout -q main # WIP: worktree, ahead, dirty tracked file git worktree add -q "$SB/wt-wip" -b feat/wip main >/dev/null 2>&1 echo more > "$SB/wt-wip/x2"; git -C "$SB/wt-wip" add -A; git -C "$SB/wt-wip" commit -qm ahead echo dirty >> "$SB/wt-wip/f" # f is tracked (from mkrepo) → unstaged change # --active-window 0 disables live-writer detection so fresh worktrees don't all read ACTIVE. OUT="$(bash "$LA" "$R" --porcelain --active-window 0 --recent-days 7)" sof(){ printf '%s\n' "$OUT" | awk -F'\t' -v b="$1" '$2==b{print $1}'; } [ "$(sof feat/land-me)" = LANDABLE ] && ok "feat/land-me → LANDABLE" || no "land-me got [$(sof feat/land-me)]" [ "$(sof feat/merged)" = MERGED ] && ok "feat/merged → MERGED" || no "merged got [$(sof feat/merged)]" [ "$(sof feat/at-trunk)" = MERGED ] && ok "nothing-ahead → MERGED" || no "at-trunk got [$(sof feat/at-trunk)]" [ "$(sof feat/stale)" = STALE ] && ok "feat/stale → STALE" || no "stale got [$(sof feat/stale)]" [ "$(sof feat/wip)" = WIP ] && ok "feat/wip → WIP" || no "wip got [$(sof feat/wip)]" badnf=$(printf '%s\n' "$OUT" | awk -F'\t' 'NF!=7{c++} END{print c+0}') [ "$badnf" = 0 ] && ok "porcelain 7 fields/line" || no "porcelain NF!=7 on $badnf line(s)" echo "-- land-all: exit 1 when LANDABLE exists; live-writer → ACTIVE --" bash "$LA" "$R" --active-window 0 >/dev/null 2>&1; [ $? -eq 1 ] && ok "exit 1 (landable available)" || no "expected exit 1" # A wide active window makes the freshly-written feat/wip worktree read as a live writer. OUT2="$(bash "$LA" "$R" --porcelain --active-window 3600 --recent-days 7)" a2=$(printf '%s\n' "$OUT2" | awk -F'\t' '$2=="feat/wip"{print $1}') [ "$a2" = ACTIVE ] && ok "fresh worktree → ACTIVE (live-writer guard)" || no "expected ACTIVE got [$a2]" echo "=== $P passed, $F failed ===" [ $F -eq 0 ]