e2e.sh 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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 .claude/fleet/worktrees/alpha ]] && ok "alpha worktree created" || fail "alpha worktree missing"
  47. [[ -d .claude/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. [[ "$(cat .claude/fleet/lanes/alpha)" == "RUNNING" ]] && ok "alpha state = RUNNING" || fail "alpha state wrong"
  51. [[ "$(cat .claude/fleet/lanes/beta)" == "RUNNING" ]] && ok "beta state = RUNNING" || fail "beta state wrong"
  52. # ── work in alpha lane ──
  53. step "do work in alpha worktree, signal READY"
  54. (
  55. cd .claude/fleet/worktrees/alpha
  56. echo "alpha feature" > a.txt
  57. git add . && git -c user.email=e2e@test -c user.name=e2e commit -q -m "feat: alpha"
  58. )
  59. echo "0 failed, 1 passed" > "$SCRATCH/alpha-test.log"
  60. ( cd .claude/fleet/worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/alpha-test.log" >/dev/null )
  61. [[ "$(head -n1 .claude/fleet/lanes/alpha)" == "READY" ]] && ok "alpha state = READY after signal" || fail "alpha not READY"
  62. step "signal.sh refuses dirty tree"
  63. (
  64. cd .claude/fleet/worktrees/alpha
  65. echo "uncommitted change" >> a.txt
  66. )
  67. ( cd .claude/fleet/worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/alpha-test.log" 2>/dev/null ) \
  68. && fail "signal.sh accepted dirty tree" || ok "signal.sh refused dirty tree"
  69. ( cd .claude/fleet/worktrees/alpha && git checkout -- a.txt ) # clean back up
  70. step "signal.sh refuses failing test log"
  71. echo "ERROR: 3 tests failed" > "$SCRATCH/bad-test.log"
  72. ( cd .claude/fleet/worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/bad-test.log" 2>/dev/null ) \
  73. && fail "signal.sh accepted failing log" || ok "signal.sh refused failing log"
  74. # Re-signal with good log to reset state for daemon test
  75. ( cd .claude/fleet/worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/alpha-test.log" >/dev/null )
  76. # ── work in beta lane ──
  77. step "do work in beta worktree, signal READY"
  78. (
  79. cd .claude/fleet/worktrees/beta
  80. echo "beta feature" > b.txt
  81. git add . && git -c user.email=e2e@test -c user.name=e2e commit -q -m "feat: beta"
  82. )
  83. echo "0 failed, 2 passed" > "$SCRATCH/beta-test.log"
  84. ( cd .claude/fleet/worktrees/beta && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/beta-test.log" >/dev/null )
  85. # ── daemon ──
  86. step "start daemon (background) and watch it land both lanes"
  87. ( cd "$SCRATCH" && bash "$FLEET" start ) &
  88. DAEMON_PID=$!
  89. note "wrapper PID: $DAEMON_PID"
  90. sleep 4
  91. # The daemon may finish before we sleep — verify via log instead
  92. if grep -q "daemon start (pid " .claude/fleet/activity.log; then
  93. ok "daemon.pid recorded in activity log"
  94. else
  95. fail "daemon never logged a start"
  96. fi
  97. # Wait up to 15s for both lanes to land or daemon to exit
  98. for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
  99. alpha_state=$(head -n1 .claude/fleet/lanes/alpha 2>/dev/null || echo "MISSING")
  100. beta_state=$(head -n1 .claude/fleet/lanes/beta 2>/dev/null || echo "MISSING")
  101. [[ "$alpha_state" == "LANDED" && "$beta_state" == "LANDED" ]] && break
  102. sleep 1
  103. done
  104. [[ "$(head -n1 .claude/fleet/lanes/alpha)" == "LANDED" ]] && ok "alpha LANDED" || fail "alpha = $(head -n1 .claude/fleet/lanes/alpha)"
  105. [[ "$(head -n1 .claude/fleet/lanes/beta)" == "LANDED" ]] && ok "beta LANDED" || fail "beta = $(head -n1 .claude/fleet/lanes/beta)"
  106. # Verify merge commits on main
  107. git -C "$SCRATCH" log --oneline main | grep -q "merge: alpha" && ok "merge: alpha commit on main" || fail "no merge: alpha commit"
  108. git -C "$SCRATCH" log --oneline main | grep -q "merge: beta" && ok "merge: beta commit on main" || fail "no merge: beta commit"
  109. # Daemon should self-exit when all lanes terminal
  110. sleep 2
  111. if [[ -f .claude/fleet/daemon.pid ]]; then
  112. fail "daemon.pid still present after all lanes terminal"
  113. else
  114. ok "daemon.pid removed after self-exit"
  115. fi
  116. wait "$DAEMON_PID" 2>/dev/null || true
  117. # ── refuse double-start ──
  118. step "refuse second daemon while one is running"
  119. output=$( ( cd "$SCRATCH" && bash "$FLEET" start ) 2>&1 || true )
  120. if echo "$output" | grep -qiE "(already running|all lanes terminal|daemon exiting)"; then
  121. ok "second start handled (refused or terminal)"
  122. else
  123. fail "second start unexpected"
  124. note "actual output: $output"
  125. fi
  126. pkill -f "fleet.sh start" 2>/dev/null || true
  127. sleep 1
  128. # ── revert ──
  129. step "fleet revert backs out a landed merge"
  130. bash "$FLEET" revert alpha >/dev/null 2>&1
  131. git -C "$SCRATCH" log --oneline main | head -1 | grep -qi "revert" && ok "revert commit created on main" || fail "no revert commit"
  132. # ── scrub-check ──
  133. step "scrub-check catches forbidden patterns"
  134. git -C "$SCRATCH" checkout -b scrub-test main >/dev/null 2>&1
  135. echo "// TODO_SCRUB: remove before landing" > scrub.txt
  136. git -C "$SCRATCH" add scrub.txt
  137. git -C "$SCRATCH" -c user.email=e2e@test -c user.name=e2e commit -q -m "test: scrub"
  138. # scrub-check exits non-zero on hits (intended) — capture output before grep to avoid pipefail
  139. scrub_out=$(bash "$FLEET" scrub-check scrub-test 2>&1 || true)
  140. echo "$scrub_out" | grep -q "FORBIDDEN" && ok "scrub-check flagged TODO_SCRUB" || fail "scrub-check missed pattern"
  141. git -C "$SCRATCH" checkout main >/dev/null 2>&1
  142. # ── ASCII fallback ──
  143. step "FLEET_ASCII=1 swaps glyphs to ASCII"
  144. ascii_out=$(FLEET_ASCII=1 bash "$FLEET" fleet 2>&1 || true)
  145. # Tree connectors carry the ASCII signal now (+- / `-); group headers
  146. # no longer carry icons (they sat at the junction and broke the tree).
  147. echo "$ascii_out" | grep -qE '\+-|`-' && ok "ASCII tree connectors rendered" || fail "ASCII connectors not used"
  148. echo "$ascii_out" | grep -qE '├─|└─|│' && fail "Unicode connectors leaked in ASCII mode" || ok "no Unicode in ASCII mode"
  149. # ── verbose view ──
  150. step "fleet fleet --verbose shows per-lane detail"
  151. verbose_out=$(bash "$FLEET" fleet --verbose 2>&1 || true)
  152. echo "$verbose_out" | grep -q "verbose" && ok "verbose header present" || fail "no verbose header"
  153. echo "$verbose_out" | grep -q "worktree:" && ok "verbose shows worktree path" || fail "no worktree path in verbose"
  154. # ── works from inside a worktree (cwd-bug regression test) ──
  155. step "fleet fleet works from inside a worktree"
  156. wt_out=$( cd "$SCRATCH/.claude/fleet/worktrees/alpha" 2>/dev/null && bash "$FLEET" fleet 2>&1 || true )
  157. echo "$wt_out" | grep -q "alpha" && ok "fleet view from worktree finds lanes" || fail "fleet view from worktree empty"
  158. # ── summary ──
  159. echo ""
  160. echo "═══════════════════════════════════════"
  161. echo " ${GREEN}PASS: $PASS${OFF} ${RED}FAIL: $FAIL${OFF}"
  162. echo "═══════════════════════════════════════"
  163. [[ $FAIL -eq 0 ]] && exit 0 || exit 1