| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- #!/usr/bin/env bash
- # fleet-ops e2e test — full lifecycle in a throwaway repo
- # Run from any cwd. Tears down its own scratch dir.
- set -euo pipefail
- SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)/skills/fleet-ops"
- FLEET="$SKILL_DIR/scripts/fleet.sh"
- SCRATCH="${TMPDIR:-/tmp}/fleet-ops-e2e-$$"
- PASS=0
- FAIL=0
- # colors (fall back if no terminal)
- if [[ -t 1 ]]; then
- GREEN=$'\033[32m'; RED=$'\033[31m'; CYAN=$'\033[36m'; DIM=$'\033[2m'; OFF=$'\033[0m'
- else
- GREEN=""; RED=""; CYAN=""; DIM=""; OFF=""
- fi
- step() { echo ""; echo "${CYAN}── $* ──${OFF}"; }
- ok() { echo "${GREEN}PASS${OFF}: $*"; PASS=$((PASS+1)); }
- fail() { echo "${RED}FAIL${OFF}: $*"; FAIL=$((FAIL+1)); }
- note() { echo "${DIM} $*${OFF}"; }
- cleanup() {
- # Kill any daemons we spawned
- if [[ -f "$SCRATCH/.claude/fleet/daemon.pid" ]]; then
- local pid
- pid=$(cat "$SCRATCH/.claude/fleet/daemon.pid" 2>/dev/null || echo "")
- [[ -n "$pid" ]] && kill -TERM "$pid" 2>/dev/null || true
- fi
- pkill -f "fleet.sh start" 2>/dev/null || true
- rm -rf "$SCRATCH"
- }
- trap cleanup EXIT
- echo "fleet-ops e2e test"
- echo " skill: $SKILL_DIR"
- echo " scratch: $SCRATCH"
- # ── setup ──
- step "setup mock repo"
- mkdir -p "$SCRATCH"
- cd "$SCRATCH"
- git init -b main -q
- echo "init" > README.md
- git add . && git -c user.email=e2e@test -c user.name=e2e commit -q -m init
- note "repo at $SCRATCH"
- # ── init ──
- step "fleet init alpha beta"
- bash "$FLEET" init alpha beta >/dev/null 2>&1
- [[ -d .claude/fleet/lanes ]] && ok "lanes/ created" || fail "lanes/ missing"
- [[ -d .fleet-worktrees/alpha ]] && ok "alpha worktree created" || fail "alpha worktree missing"
- [[ -d .fleet-worktrees/beta ]] && ok "beta worktree created" || fail "beta worktree missing"
- [[ -f .claude/fleet/signal.sh ]] && ok "signal.sh deployed" || fail "signal.sh not deployed"
- grep -qxF '.claude/fleet/' .gitignore && ok ".claude/fleet/ in .gitignore" || fail ".gitignore not updated"
- grep -qxF '.fleet-worktrees/' .gitignore && ok ".fleet-worktrees/ in .gitignore" || fail ".fleet-worktrees/ not in .gitignore"
- [[ "$(cat .claude/fleet/lanes/alpha)" == "RUNNING" ]] && ok "alpha state = RUNNING" || fail "alpha state wrong"
- [[ "$(cat .claude/fleet/lanes/beta)" == "RUNNING" ]] && ok "beta state = RUNNING" || fail "beta state wrong"
- # ── track (native-spawn path) ──
- step "fleet track registers an existing branch as a lane"
- git branch gamma main
- bash "$FLEET" track gamma >/dev/null 2>&1
- [[ -f .claude/fleet/lanes/gamma ]] && ok "gamma lane file created" || fail "gamma lane missing"
- [[ "$(head -n1 .claude/fleet/lanes/gamma 2>/dev/null)" == "RUNNING" ]] && ok "gamma state = RUNNING" || fail "gamma state wrong"
- [[ -d .fleet-worktrees/gamma ]] && fail "track created a worktree (it must not)" || ok "track created no worktree"
- bash "$FLEET" track no-such-branch >/dev/null 2>&1 && fail "track accepted missing branch" || ok "track refused missing branch"
- # untrack gamma so it doesn't block daemon self-exit later
- git branch -D gamma >/dev/null 2>&1
- rm -f .claude/fleet/lanes/gamma
- # ── work in alpha lane ──
- step "do work in alpha worktree, signal READY"
- (
- cd .fleet-worktrees/alpha
- echo "alpha feature" > a.txt
- git add . && git -c user.email=e2e@test -c user.name=e2e commit -q -m "feat: alpha"
- )
- echo "0 failed, 1 passed" > "$SCRATCH/alpha-test.log"
- ( cd .fleet-worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/alpha-test.log" >/dev/null )
- [[ "$(head -n1 .claude/fleet/lanes/alpha)" == "READY" ]] && ok "alpha state = READY after signal" || fail "alpha not READY"
- step "signal.sh refuses dirty tree"
- (
- cd .fleet-worktrees/alpha
- echo "uncommitted change" >> a.txt
- )
- ( cd .fleet-worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/alpha-test.log" 2>/dev/null ) \
- && fail "signal.sh accepted dirty tree" || ok "signal.sh refused dirty tree"
- ( cd .fleet-worktrees/alpha && git checkout -- a.txt ) # clean back up
- step "signal.sh refuses failing test log"
- echo "ERROR: 3 tests failed" > "$SCRATCH/bad-test.log"
- ( cd .fleet-worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/bad-test.log" 2>/dev/null ) \
- && fail "signal.sh accepted failing log" || ok "signal.sh refused failing log"
- # Re-signal with good log to reset state for daemon test
- ( cd .fleet-worktrees/alpha && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/alpha-test.log" >/dev/null )
- # ── work in beta lane ──
- step "do work in beta worktree, signal READY"
- (
- cd .fleet-worktrees/beta
- echo "beta feature" > b.txt
- git add . && git -c user.email=e2e@test -c user.name=e2e commit -q -m "feat: beta"
- )
- echo "0 failed, 2 passed" > "$SCRATCH/beta-test.log"
- ( cd .fleet-worktrees/beta && bash "$SCRATCH/.claude/fleet/signal.sh" READY "$SCRATCH/beta-test.log" >/dev/null )
- # ── daemon ──
- step "start daemon (background) and watch it land both lanes"
- ( cd "$SCRATCH" && bash "$FLEET" start ) &
- DAEMON_PID=$!
- note "wrapper PID: $DAEMON_PID"
- sleep 4
- # The daemon may finish before we sleep — verify via log instead
- if grep -q "daemon start (pid " .claude/fleet/activity.log; then
- ok "daemon.pid recorded in activity log"
- else
- fail "daemon never logged a start"
- fi
- # Wait up to 15s for both lanes to land or daemon to exit
- for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
- alpha_state=$(head -n1 .claude/fleet/lanes/alpha 2>/dev/null || echo "MISSING")
- beta_state=$(head -n1 .claude/fleet/lanes/beta 2>/dev/null || echo "MISSING")
- [[ "$alpha_state" == "LANDED" && "$beta_state" == "LANDED" ]] && break
- sleep 1
- done
- [[ "$(head -n1 .claude/fleet/lanes/alpha)" == "LANDED" ]] && ok "alpha LANDED" || fail "alpha = $(head -n1 .claude/fleet/lanes/alpha)"
- [[ "$(head -n1 .claude/fleet/lanes/beta)" == "LANDED" ]] && ok "beta LANDED" || fail "beta = $(head -n1 .claude/fleet/lanes/beta)"
- # Verify merge commits on main
- git -C "$SCRATCH" log --oneline main | grep -q "merge: alpha" && ok "merge: alpha commit on main" || fail "no merge: alpha commit"
- git -C "$SCRATCH" log --oneline main | grep -q "merge: beta" && ok "merge: beta commit on main" || fail "no merge: beta commit"
- # Daemon should self-exit when all lanes terminal
- sleep 2
- if [[ -f .claude/fleet/daemon.pid ]]; then
- fail "daemon.pid still present after all lanes terminal"
- else
- ok "daemon.pid removed after self-exit"
- fi
- wait "$DAEMON_PID" 2>/dev/null || true
- # ── refuse double-start ──
- step "refuse second daemon while one is running"
- output=$( ( cd "$SCRATCH" && bash "$FLEET" start ) 2>&1 || true )
- if echo "$output" | grep -qiE "(already running|all lanes terminal|daemon exiting)"; then
- ok "second start handled (refused or terminal)"
- else
- fail "second start unexpected"
- note "actual output: $output"
- fi
- pkill -f "fleet.sh start" 2>/dev/null || true
- sleep 1
- # ── revert ──
- step "fleet revert backs out a landed merge"
- bash "$FLEET" revert alpha >/dev/null 2>&1
- git -C "$SCRATCH" log --oneline main | head -1 | grep -qi "revert" && ok "revert commit created on main" || fail "no revert commit"
- # ── scrub-check ──
- step "scrub-check catches forbidden patterns"
- git -C "$SCRATCH" checkout -b scrub-test main >/dev/null 2>&1
- echo "// TODO_SCRUB: remove before landing" > scrub.txt
- git -C "$SCRATCH" add scrub.txt
- git -C "$SCRATCH" -c user.email=e2e@test -c user.name=e2e commit -q -m "test: scrub"
- # scrub-check exits non-zero on hits (intended) — capture output before grep to avoid pipefail
- scrub_out=$(bash "$FLEET" scrub-check scrub-test 2>&1 || true)
- echo "$scrub_out" | grep -q "FORBIDDEN" && ok "scrub-check flagged TODO_SCRUB" || fail "scrub-check missed pattern"
- git -C "$SCRATCH" checkout main >/dev/null 2>&1
- # ── ASCII fallback ──
- step "FLEET_ASCII=1 swaps glyphs to ASCII"
- ascii_out=$(FLEET_ASCII=1 bash "$FLEET" fleet 2>&1 || true)
- # Tree connectors carry the ASCII signal now (+- / `-); group headers
- # no longer carry icons (they sat at the junction and broke the tree).
- echo "$ascii_out" | grep -qE '\+-|`-' && ok "ASCII tree connectors rendered" || fail "ASCII connectors not used"
- echo "$ascii_out" | grep -qE '├─|└─|│' && fail "Unicode connectors leaked in ASCII mode" || ok "no Unicode in ASCII mode"
- # ── verbose view ──
- step "fleet fleet --verbose shows per-lane detail"
- verbose_out=$(bash "$FLEET" fleet --verbose 2>&1 || true)
- echo "$verbose_out" | grep -q "verbose" && ok "verbose header present" || fail "no verbose header"
- echo "$verbose_out" | grep -q "worktree:" && ok "verbose shows worktree path" || fail "no worktree path in verbose"
- # ── works from inside a worktree (cwd-bug regression test) ──
- step "fleet fleet works from inside a worktree"
- wt_out=$( cd "$SCRATCH/.fleet-worktrees/alpha" 2>/dev/null && bash "$FLEET" fleet 2>&1 || true )
- echo "$wt_out" | grep -q "alpha" && ok "fleet view from worktree finds lanes" || fail "fleet view from worktree empty"
- # ── summary ──
- echo ""
- echo "═══════════════════════════════════════"
- echo " ${GREEN}PASS: $PASS${OFF} ${RED}FAIL: $FAIL${OFF}"
- echo "═══════════════════════════════════════"
- [[ $FAIL -eq 0 ]] && exit 0 || exit 1
|