e2e.sh 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. #!/usr/bin/env bash
  2. # fleet-ops e2e test — full lifecycle in a throwaway repo
  3. # Run from any cwd. Tears down its own scratch dir.
  4. set -euo pipefail
  5. SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)/skills/fleet-ops"
  6. FLEET="$SKILL_DIR/scripts/fleet.sh"
  7. SCRATCH="${TMPDIR:-/tmp}/fleet-ops-e2e-$$"
  8. PASS=0
  9. FAIL=0
  10. # colors (fall back if no terminal)
  11. if [[ -t 1 ]]; then
  12. GREEN=$'\033[32m'; RED=$'\033[31m'; CYAN=$'\033[36m'; DIM=$'\033[2m'; OFF=$'\033[0m'
  13. else
  14. GREEN=""; RED=""; CYAN=""; DIM=""; OFF=""
  15. fi
  16. step() { echo ""; echo "${CYAN}── $* ──${OFF}"; }
  17. ok() { echo "${GREEN}PASS${OFF}: $*"; PASS=$((PASS+1)); }
  18. fail() { echo "${RED}FAIL${OFF}: $*"; FAIL=$((FAIL+1)); }
  19. note() { echo "${DIM} $*${OFF}"; }
  20. cleanup() {
  21. # Kill any daemons we spawned
  22. if [[ -f "$SCRATCH/.claude/fleet/daemon.pid" ]]; then
  23. local pid
  24. pid=$(cat "$SCRATCH/.claude/fleet/daemon.pid" 2>/dev/null || echo "")
  25. [[ -n "$pid" ]] && kill -TERM "$pid" 2>/dev/null || true
  26. fi
  27. pkill -f "fleet.sh start" 2>/dev/null || true
  28. rm -rf "$SCRATCH"
  29. }
  30. trap cleanup EXIT
  31. echo "fleet-ops e2e test"
  32. echo " skill: $SKILL_DIR"
  33. echo " scratch: $SCRATCH"
  34. # ── setup ──
  35. step "setup mock repo"
  36. mkdir -p "$SCRATCH"
  37. cd "$SCRATCH"
  38. git init -b main -q
  39. echo "init" > README.md
  40. git add . && git -c user.email=e2e@test -c user.name=e2e commit -q -m init
  41. note "repo at $SCRATCH"
  42. # ── init ──
  43. step "fleet init alpha beta"
  44. bash "$FLEET" init alpha beta >/dev/null 2>&1
  45. [[ -d .claude/fleet/lanes ]] && ok "lanes/ created" || fail "lanes/ missing"
  46. [[ -d .fleet-worktrees/alpha ]] && ok "alpha worktree created" || fail "alpha worktree missing"
  47. [[ -d .fleet-worktrees/beta ]] && ok "beta worktree created" || fail "beta worktree missing"
  48. [[ -f .claude/fleet/signal.sh ]] && ok "signal.sh deployed" || fail "signal.sh not deployed"
  49. grep -qxF '.claude/fleet/' .gitignore && ok ".claude/fleet/ in .gitignore" || fail ".gitignore not updated"
  50. grep -qxF '.fleet-worktrees/' .gitignore && ok ".fleet-worktrees/ in .gitignore" || fail ".fleet-worktrees/ not in .gitignore"
  51. [[ "$(cat .claude/fleet/lanes/alpha)" == "RUNNING" ]] && ok "alpha state = RUNNING" || fail "alpha state wrong"
  52. [[ "$(cat .claude/fleet/lanes/beta)" == "RUNNING" ]] && ok "beta state = RUNNING" || fail "beta state wrong"
  53. # ── track (native-spawn path) ──
  54. step "fleet track registers an existing branch as a lane"
  55. git branch gamma main
  56. bash "$FLEET" track gamma >/dev/null 2>&1
  57. [[ -f .claude/fleet/lanes/gamma ]] && ok "gamma lane file created" || fail "gamma lane missing"
  58. [[ "$(head -n1 .claude/fleet/lanes/gamma 2>/dev/null)" == "RUNNING" ]] && ok "gamma state = RUNNING" || fail "gamma state wrong"
  59. [[ -d .fleet-worktrees/gamma ]] && fail "track created a worktree (it must not)" || ok "track created no worktree"
  60. bash "$FLEET" track no-such-branch >/dev/null 2>&1 && fail "track accepted missing branch" || ok "track refused missing branch"
  61. # untrack gamma so it doesn't block daemon self-exit later
  62. git branch -D gamma >/dev/null 2>&1
  63. rm -f .claude/fleet/lanes/gamma
  64. # ── work in alpha lane ──
  65. step "do work in alpha worktree, signal READY"
  66. (
  67. cd .fleet-worktrees/alpha
  68. echo "alpha feature" > a.txt
  69. git add . && git -c user.email=e2e@test -c user.name=e2e commit -q -m "feat: alpha"
  70. )
  71. echo "0 failed, 1 passed" > "$SCRATCH/alpha-test.log"
  72. ( cd .fleet-worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/alpha-test.log" >/dev/null )
  73. [[ "$(head -n1 .claude/fleet/lanes/alpha)" == "READY" ]] && ok "alpha state = READY after signal" || fail "alpha not READY"
  74. step "signal.sh refuses dirty tree"
  75. (
  76. cd .fleet-worktrees/alpha
  77. echo "uncommitted change" >> a.txt
  78. )
  79. ( cd .fleet-worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/alpha-test.log" 2>/dev/null ) \
  80. && fail "signal.sh accepted dirty tree" || ok "signal.sh refused dirty tree"
  81. ( cd .fleet-worktrees/alpha && git checkout -- a.txt ) # clean back up
  82. step "signal.sh refuses failing test log"
  83. echo "ERROR: 3 tests failed" > "$SCRATCH/bad-test.log"
  84. ( cd .fleet-worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/bad-test.log" 2>/dev/null ) \
  85. && fail "signal.sh accepted failing log" || ok "signal.sh refused failing log"
  86. # Re-signal with good log to reset state for daemon test
  87. ( cd .fleet-worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/alpha-test.log" >/dev/null )
  88. # ── work in beta lane ──
  89. step "do work in beta worktree, signal READY"
  90. (
  91. cd .fleet-worktrees/beta
  92. echo "beta feature" > b.txt
  93. git add . && git -c user.email=e2e@test -c user.name=e2e commit -q -m "feat: beta"
  94. )
  95. echo "0 failed, 2 passed" > "$SCRATCH/beta-test.log"
  96. ( cd .fleet-worktrees/beta && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/beta-test.log" >/dev/null )
  97. # ── daemon ──
  98. step "start daemon (background) and watch it land both lanes"
  99. ( cd "$SCRATCH" && bash "$FLEET" start ) &
  100. DAEMON_PID=$!
  101. note "wrapper PID: $DAEMON_PID"
  102. sleep 4
  103. # The daemon may finish before we sleep — verify via log instead
  104. if grep -q "daemon start (pid " .claude/fleet/activity.log; then
  105. ok "daemon.pid recorded in activity log"
  106. else
  107. fail "daemon never logged a start"
  108. fi
  109. # Wait up to 15s for both lanes to land or daemon to exit
  110. for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
  111. alpha_state=$(head -n1 .claude/fleet/lanes/alpha 2>/dev/null || echo "MISSING")
  112. beta_state=$(head -n1 .claude/fleet/lanes/beta 2>/dev/null || echo "MISSING")
  113. [[ "$alpha_state" == "LANDED" && "$beta_state" == "LANDED" ]] && break
  114. sleep 1
  115. done
  116. [[ "$(head -n1 .claude/fleet/lanes/alpha)" == "LANDED" ]] && ok "alpha LANDED" || fail "alpha = $(head -n1 .claude/fleet/lanes/alpha)"
  117. [[ "$(head -n1 .claude/fleet/lanes/beta)" == "LANDED" ]] && ok "beta LANDED" || fail "beta = $(head -n1 .claude/fleet/lanes/beta)"
  118. # Verify merge commits on main
  119. git -C "$SCRATCH" log --oneline main | grep -q "merge: alpha" && ok "merge: alpha commit on main" || fail "no merge: alpha commit"
  120. git -C "$SCRATCH" log --oneline main | grep -q "merge: beta" && ok "merge: beta commit on main" || fail "no merge: beta commit"
  121. # Daemon should self-exit when all lanes terminal
  122. sleep 2
  123. if [[ -f .claude/fleet/daemon.pid ]]; then
  124. fail "daemon.pid still present after all lanes terminal"
  125. else
  126. ok "daemon.pid removed after self-exit"
  127. fi
  128. wait "$DAEMON_PID" 2>/dev/null || true
  129. # ── refuse double-start ──
  130. step "refuse second daemon while one is running"
  131. output=$( ( cd "$SCRATCH" && bash "$FLEET" start ) 2>&1 || true )
  132. if echo "$output" | grep -qiE "(already running|all lanes terminal|daemon exiting)"; then
  133. ok "second start handled (refused or terminal)"
  134. else
  135. fail "second start unexpected"
  136. note "actual output: $output"
  137. fi
  138. pkill -f "fleet.sh start" 2>/dev/null || true
  139. sleep 1
  140. # ── revert ──
  141. step "fleet revert backs out a landed merge"
  142. bash "$FLEET" revert alpha >/dev/null 2>&1
  143. git -C "$SCRATCH" log --oneline main | head -1 | grep -qi "revert" && ok "revert commit created on main" || fail "no revert commit"
  144. # ── scrub-check ──
  145. step "scrub-check catches forbidden patterns"
  146. git -C "$SCRATCH" checkout -b scrub-test main >/dev/null 2>&1
  147. echo "// TODO_SCRUB: remove before landing" > scrub.txt
  148. git -C "$SCRATCH" add scrub.txt
  149. git -C "$SCRATCH" -c user.email=e2e@test -c user.name=e2e commit -q -m "test: scrub"
  150. # scrub-check exits non-zero on hits (intended) — capture output before grep to avoid pipefail
  151. scrub_out=$(bash "$FLEET" scrub-check scrub-test 2>&1 || true)
  152. echo "$scrub_out" | grep -q "FORBIDDEN" && ok "scrub-check flagged TODO_SCRUB" || fail "scrub-check missed pattern"
  153. git -C "$SCRATCH" checkout main >/dev/null 2>&1
  154. # ── ASCII fallback ──
  155. step "FLEET_ASCII=1 swaps glyphs to ASCII"
  156. ascii_out=$(FLEET_ASCII=1 bash "$FLEET" fleet 2>&1 || true)
  157. # Tree connectors carry the ASCII signal now (+- / `-); group headers
  158. # no longer carry icons (they sat at the junction and broke the tree).
  159. echo "$ascii_out" | grep -qE '\+-|`-' && ok "ASCII tree connectors rendered" || fail "ASCII connectors not used"
  160. echo "$ascii_out" | grep -qE '├─|└─|│' && fail "Unicode connectors leaked in ASCII mode" || ok "no Unicode in ASCII mode"
  161. # ── verbose view ──
  162. step "fleet fleet --verbose shows per-lane detail"
  163. verbose_out=$(bash "$FLEET" fleet --verbose 2>&1 || true)
  164. echo "$verbose_out" | grep -q "verbose" && ok "verbose header present" || fail "no verbose header"
  165. echo "$verbose_out" | grep -q "worktree:" && ok "verbose shows worktree path" || fail "no worktree path in verbose"
  166. # ── works from inside a worktree (cwd-bug regression test) ──
  167. step "fleet fleet works from inside a worktree"
  168. wt_out=$( cd "$SCRATCH/.fleet-worktrees/alpha" 2>/dev/null && bash "$FLEET" fleet 2>&1 || true )
  169. echo "$wt_out" | grep -q "alpha" && ok "fleet view from worktree finds lanes" || fail "fleet view from worktree empty"
  170. # ── summary ──
  171. echo ""
  172. echo "═══════════════════════════════════════"
  173. echo " ${GREEN}PASS: $PASS${OFF} ${RED}FAIL: $FAIL${OFF}"
  174. echo "═══════════════════════════════════════"
  175. [[ $FAIL -eq 0 ]] && exit 0 || exit 1