run.sh 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. #!/usr/bin/env bash
  2. # Self-test for git-ops worktree provisioning. Offline + deterministic (git only,
  3. # no network). Covers new-lane.sh (in-repo default, gitignore precondition,
  4. # --sibling, main-anchoring, validate-before-mutate, arg hygiene) and the
  5. # worktree-guard.sh `git clean` double-force detection.
  6. # Resolves paths relative to itself so it runs in the repo and once installed.
  7. #
  8. # Usage: bash tests/run.sh
  9. # Exit: 0 all pass, 1 one or more failures (SKIP+exit 0 if git is unavailable)
  10. set -uo pipefail
  11. HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  12. SKILL="$(dirname "$HERE")"
  13. NL="$SKILL/scripts/new-lane.sh"
  14. # worktree-guard.sh lives in the repo's hooks/ — resolve for both repo + installed layouts.
  15. GUARD=""
  16. for c in "$SKILL/../../hooks/worktree-guard.sh" "$HOME/.claude/hooks/worktree-guard.sh"; do
  17. [ -f "$c" ] && { GUARD="$c"; break; }
  18. done
  19. command -v git >/dev/null 2>&1 || { echo "SKIP: git not available"; exit 0; }
  20. SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
  21. P=0; F=0
  22. ok(){ P=$((P+1)); printf ' PASS %s\n' "$1"; }
  23. no(){ F=$((F+1)); printf ' FAIL %s\n' "$1"; }
  24. mkrepo(){ rm -rf "$1"; git init -q -b main "$1"; git -C "$1" config user.email t@t
  25. git -C "$1" config user.name t; git -C "$1" config core.autocrlf false
  26. echo base > "$1/f"; git -C "$1" add -A; git -C "$1" commit -qm init; }
  27. echo "=== git-ops new-lane / worktree-guard self-test ==="
  28. echo "-- new-lane: in-repo default + gitignore precondition --"
  29. mkrepo "$SB/A"; cd "$SB/A"
  30. OUT="$(bash "$NL" auth-refactor 2>/dev/null)"; rc=$?
  31. [ $rc -eq 0 ] && ok "exit 0" || no "exit $rc"
  32. case "$OUT" in */.claude/worktrees/auth-refactor) ok "stdout = in-repo path only";; *) no "stdout=[$OUT]";; esac
  33. [ -d "$SB/A/.claude/worktrees/auth-refactor" ] && ok "lane dir created in-repo" || no "lane dir missing"
  34. git check-ignore -q .claude/worktrees/.x && ok ".claude/worktrees/ now gitignored" || no "gitignore not ensured"
  35. git show-ref --verify --quiet refs/heads/lane/auth-refactor && ok "branch lane/auth-refactor" || no "branch missing"
  36. echo "-- new-lane: no duplicate gitignore on second lane --"
  37. before=$(grep -c worktrees .gitignore); bash "$NL" hotfix main >/dev/null 2>&1
  38. [ "$before" = "$(grep -c worktrees .gitignore)" ] && ok "gitignore not duplicated" || no "gitignore duplicated"
  39. echo "-- new-lane: blanket .claude/ ignore is respected (no append) --"
  40. mkrepo "$SB/B"; cd "$SB/B"; printf '.claude/\n' > .gitignore; git add .gitignore; git commit -qm ig
  41. sz=$(wc -c <.gitignore); bash "$NL" x >/dev/null 2>&1
  42. [ "$sz" = "$(wc -c <.gitignore)" ] && ok "blanket .claude/ -> gitignore untouched" || no "appended despite blanket ignore"
  43. echo "-- new-lane: --sibling places outside the repo --"
  44. mkrepo "$SB/S"; cd "$SB/S"
  45. OS="$(bash "$NL" --sibling big 2>/dev/null)"
  46. [ "$OS" = "$SB/S-big" ] && ok "sibling path = $OS" || no "sibling=[$OS]"
  47. [ -d "$SB/S-big" ] && ok "sibling dir outside repo" || no "sibling dir missing"
  48. echo "-- new-lane: anchors at MAIN when invoked from inside a lane (no nesting) --"
  49. mkrepo "$SB/N"; cd "$SB/N"; L1="$(bash "$NL" first 2>/dev/null)"
  50. ( cd "$L1" && bash "$NL" second >/dev/null 2>&1 )
  51. [ -d "$SB/N/.claude/worktrees/second" ] && ok "lane anchored to MAIN" || no "did not anchor to MAIN"
  52. [ ! -d "$L1/.claude/worktrees/second" ] && ok "no nested worktree inside the lane" || no "nested worktree leaked"
  53. echo "-- new-lane: validate-before-mutate + arg hygiene --"
  54. mkrepo "$SB/V"; cd "$SB/V"
  55. bash "$NL" lane1 no-such-branch >/dev/null 2>/dev/null; ee=$?
  56. [ $ee -eq 2 ] && ok "invalid base -> exit 2" || no "invalid base exit $ee"
  57. [ ! -f .gitignore ] && ok "invalid base left no .gitignore" || no "partial gitignore on bad base"
  58. bash "$NL" --frobnicate slug >/dev/null 2>/dev/null; ee=$?
  59. [ $ee -eq 2 ] && ok "unknown flag -> exit 2" || no "unknown flag exit $ee"
  60. [ -z "$(git branch --list 'lane/*')" ] && ok "unknown flag created no branch" || no "stray branch from bad flag"
  61. mkrepo "$SB/V2"; cd "$SB/V2"; git branch lane/dup
  62. bash "$NL" dup >/dev/null 2>/dev/null; ee=$?
  63. [ $ee -eq 1 ] && ok "existing branch -> exit 1" || no "dup exit $ee"
  64. [ ! -f .gitignore ] && ok "conflict left no .gitignore" || no "partial gitignore on conflict"
  65. bash "$NL" >/dev/null 2>/dev/null; [ $? -eq 2 ] && ok "no slug -> exit 2" || no "empty not exit 2"
  66. echo "-- new-lane: slug sanitised; syntax clean --"
  67. mkrepo "$SB/Z"; cd "$SB/Z"
  68. bash "$NL" "Feat Foo/Bar" >/dev/null 2>&1
  69. 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/*')"
  70. bash -n "$NL" && ok "new-lane.sh syntax OK" || no "new-lane.sh syntax error"
  71. if [ -n "$GUARD" ]; then
  72. echo "-- worktree-guard: git clean force-level detection --"
  73. bash -n "$GUARD" && ok "worktree-guard.sh syntax OK" || no "guard syntax error"
  74. WG="$SB/WG"; mkrepo "$WG"; mkdir -p "$WG/.claude/worktrees"
  75. gclean(){ local o; o=$(printf '{"tool_input":{"command":"%s"},"cwd":"%s"}' "$1" "$WG" | bash "$GUARD" 2>&1)
  76. if [ "$2" = warn ]; then case "$o" in *double-force*) ok "guard WARN: $1";; *) no "guard expected WARN: $1 [${o:-silent}]";; esac
  77. else [ -z "$o" ] && ok "guard silent: $1" || no "guard expected silent: $1"; fi; }
  78. gclean "git clean -fdx" silent
  79. gclean "git clean -ffdx" warn
  80. gclean "git clean --force --force" warn
  81. gclean "git clean -f -f" warn
  82. gclean "git clean -fd -fx" warn
  83. gclean "git clean --force -f" warn
  84. gclean "git clean -n" silent
  85. gclean "git clean -fd ./somepath" silent
  86. # exemption: a session whose cwd is inside a worktree is not warned
  87. ex=$(printf '{"tool_input":{"command":"git clean -ffdx"},"cwd":"%s/.claude/worktrees/foo"}' "$WG" | bash "$GUARD" 2>&1)
  88. [ -z "$ex" ] && ok "guard exempt inside own worktree" || no "guard warned inside own worktree"
  89. # existing rules unregressed
  90. for c in "git add -A" "rm -rf .claude/worktrees/foo" "git worktree remove .claude/worktrees/foo"; do
  91. o=$(printf '{"tool_input":{"command":"%s"},"cwd":"%s"}' "$c" "$WG" | bash "$GUARD" 2>&1)
  92. [ -n "$o" ] && ok "guard still warns: $c" || no "guard regressed: $c"
  93. done
  94. else
  95. echo " SKIP worktree-guard.sh not found (hooks/ unavailable in this layout)"
  96. fi
  97. echo "-- land-all: classification of mixed branches --"
  98. LA="$SKILL/scripts/land-all.sh"
  99. bash -n "$LA" && ok "land-all.sh syntax OK" || no "land-all.sh syntax error"
  100. R="$SB/LA"; mkrepo "$R"; cd "$R"
  101. # LANDABLE: ahead, clean, no worktree, recent commit
  102. git checkout -q -b feat/land-me; echo x > x; git add -A; git commit -qm land; git checkout -q main
  103. # MERGED: merged into trunk
  104. git checkout -q -b feat/merged; echo m > m; git add -A; git commit -qm m; git checkout -q main
  105. git merge -q --no-ff -m "merge: feat/merged" feat/merged
  106. # NOTHING-AHEAD: points at trunk → ancestor → folds into MERGED
  107. git branch feat/at-trunk main
  108. # STALE: ahead but last commit older than --recent-days (fixed past ISO date;
  109. # relative dates like "30 days ago" aren't accepted by GIT_COMMITTER_DATE on all gits)
  110. git checkout -q -b feat/stale; echo s > s; git add -A
  111. GIT_AUTHOR_DATE="2020-01-01T00:00:00" GIT_COMMITTER_DATE="2020-01-01T00:00:00" git commit -qm stale
  112. git checkout -q main
  113. # WIP: worktree, ahead, dirty tracked file
  114. git worktree add -q "$SB/wt-wip" -b feat/wip main >/dev/null 2>&1
  115. echo more > "$SB/wt-wip/x2"; git -C "$SB/wt-wip" add -A; git -C "$SB/wt-wip" commit -qm ahead
  116. echo dirty >> "$SB/wt-wip/f" # f is tracked (from mkrepo) → unstaged change
  117. # --active-window 0 disables live-writer detection so fresh worktrees don't all read ACTIVE.
  118. OUT="$(bash "$LA" "$R" --porcelain --active-window 0 --recent-days 7)"
  119. sof(){ printf '%s\n' "$OUT" | awk -F'\t' -v b="$1" '$2==b{print $1}'; }
  120. [ "$(sof feat/land-me)" = LANDABLE ] && ok "feat/land-me → LANDABLE" || no "land-me got [$(sof feat/land-me)]"
  121. [ "$(sof feat/merged)" = MERGED ] && ok "feat/merged → MERGED" || no "merged got [$(sof feat/merged)]"
  122. [ "$(sof feat/at-trunk)" = MERGED ] && ok "nothing-ahead → MERGED" || no "at-trunk got [$(sof feat/at-trunk)]"
  123. [ "$(sof feat/stale)" = STALE ] && ok "feat/stale → STALE" || no "stale got [$(sof feat/stale)]"
  124. [ "$(sof feat/wip)" = WIP ] && ok "feat/wip → WIP" || no "wip got [$(sof feat/wip)]"
  125. badnf=$(printf '%s\n' "$OUT" | awk -F'\t' 'NF!=7{c++} END{print c+0}')
  126. [ "$badnf" = 0 ] && ok "porcelain 7 fields/line" || no "porcelain NF!=7 on $badnf line(s)"
  127. echo "-- land-all: exit 1 when LANDABLE exists; live-writer → ACTIVE --"
  128. bash "$LA" "$R" --active-window 0 >/dev/null 2>&1; [ $? -eq 1 ] && ok "exit 1 (landable available)" || no "expected exit 1"
  129. # A wide active window makes the freshly-written feat/wip worktree read as a live writer.
  130. OUT2="$(bash "$LA" "$R" --porcelain --active-window 3600 --recent-days 7)"
  131. a2=$(printf '%s\n' "$OUT2" | awk -F'\t' '$2=="feat/wip"{print $1}')
  132. [ "$a2" = ACTIVE ] && ok "fresh worktree → ACTIVE (live-writer guard)" || no "expected ACTIVE got [$a2]"
  133. echo "=== $P passed, $F failed ==="
  134. [ $F -eq 0 ]