#!/usr/bin/env bash
# =============================================================================
# e2e-test.sh — Full end-to-end integration test for Timmy Tower World.
#
# Tests the complete stack:
# 1. API health check
# 2. Tower (Matrix) is served at /tower
# 3. WebSocket /api/ws handshake and event protocol
# 4. Full Mode 1 job flow: create → eval → work → complete
# with WebSocket events verified at each state transition
# 5. Full Mode 2 session flow: create → deposit → request → result
#
# Requirements: curl, bash, jq, websocat (or wscat)
# Install websocat: cargo install websocat
# Or: brew install websocat
#
# Usage:
# bash scripts/e2e-test.sh [BASE_URL]
# BASE_URL defaults to http://localhost:8080
# =============================================================================
set -euo pipefail
BASE="${1:-http://localhost:8080}"
WS_BASE="${BASE/http:/ws:}"
WS_BASE="${WS_BASE/https:/wss:}"
PASS=0; FAIL=0
note() { echo " [$1] $2"; }
sep() { echo; echo "=== $* ==="; }
body_of() { echo "$1" | sed '$d'; }
code_of() { echo "$1" | tail -n1; }
echo "Timmy Tower World — E2E Integration Test"
echo "Target: $BASE"
echo "$(date)"
echo
# ---------------------------------------------------------------------------
# 1. API health
# ---------------------------------------------------------------------------
sep "1. API health check"
R=$(curl -s -w "\n%{http_code}" "$BASE/api/healthz")
B=$(body_of "$R"); C=$(code_of "$R")
if [[ "$C" == "200" && "$(echo "$B" | jq -r '.status')" == "ok" ]]; then
note PASS "GET /api/healthz → 200, status=ok"
PASS=$((PASS+1))
else
note FAIL "code=$C body=$B"
FAIL=$((FAIL+1))
fi
# ---------------------------------------------------------------------------
# 2. Tower (Matrix frontend) served at /tower
# ---------------------------------------------------------------------------
sep "2. Tower frontend served"
TOWER_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/tower/")
if [[ "$TOWER_CODE" == "200" ]]; then
note PASS "GET /tower/ → 200"
PASS=$((PASS+1))
else
note FAIL "GET /tower/ → $TOWER_CODE (Matrix may not be built or path wrong)"
FAIL=$((FAIL+1))
fi
# Check it looks like the Matrix HTML
TOWER_TITLE=$(curl -s "$BASE/tower/" | grep -o '
[^<]*' | head -1 || echo "")
if [[ "$TOWER_TITLE" == *"Timmy Tower"* || "$TOWER_TITLE" == *"Matrix"* ]]; then
note PASS "Tower HTML contains expected title"
PASS=$((PASS+1))
else
note FAIL "Tower title unexpected: '$TOWER_TITLE'"
FAIL=$((FAIL+1))
fi
# ---------------------------------------------------------------------------
# 3. WebSocket /api/ws handshake
# ---------------------------------------------------------------------------
sep "3. WebSocket /api/ws handshake"
WS_URL="${WS_BASE}/api/ws"
if ! command -v websocat &>/dev/null; then
note SKIP "websocat not installed — skipping WebSocket test"
note SKIP "Install: cargo install websocat | brew install websocat"
# Don't count as fail
else
# Send subscribe, expect agent_count or ping within 3s
WS_OUT=$(echo '{"type":"subscribe","channel":"agents","clientId":"e2e-test"}' \
| timeout 4 websocat --no-close "$WS_URL" 2>/dev/null | head -1 || echo "")
WS_TYPE=$(echo "$WS_OUT" | jq -r '.type' 2>/dev/null || echo "")
if [[ "$WS_TYPE" == "agent_count" || "$WS_TYPE" == "ping" ]]; then
note PASS "WebSocket connected, received: type=$WS_TYPE"
PASS=$((PASS+1))
else
note FAIL "No valid WS message received (got: '$WS_OUT')"
FAIL=$((FAIL+1))
fi
fi
# ---------------------------------------------------------------------------
# 4. Mode 1 — Full job flow
# ---------------------------------------------------------------------------
sep "4. Mode 1 — Create job"
R=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/jobs" \
-H "Content-Type: application/json" \
-d '{"request":"What is a satoshi? One sentence."}')
B=$(body_of "$R"); C=$(code_of "$R")
JOB_ID=$(echo "$B" | jq -r '.jobId' 2>/dev/null || echo "")
EVAL_HASH=$(echo "$B" | jq -r '.evalInvoice.paymentHash' 2>/dev/null || echo "")
if [[ "$C" == "201" && -n "$JOB_ID" ]]; then
note PASS "POST /api/jobs → 201, jobId=$JOB_ID"
PASS=$((PASS+1))
else
note FAIL "code=$C body=$B"
FAIL=$((FAIL+1))
fi
sep "4b. Pay eval invoice (stub)"
if [[ -n "$EVAL_HASH" && "$EVAL_HASH" != "null" ]]; then
R=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/dev/stub/pay/$EVAL_HASH")
B=$(body_of "$R"); C=$(code_of "$R")
if [[ "$C" == "200" && "$(echo "$B" | jq -r '.ok')" == "true" ]]; then
note PASS "Stub pay eval → ok=true"
PASS=$((PASS+1))
else
note FAIL "code=$C body=$B"
FAIL=$((FAIL+1))
fi
else
note SKIP "No eval hash (not stub mode)"
fi
sep "4c. Poll until awaiting_work_payment"
START=$(date +%s); TIMEOUT=30; STATE=""; WORK_HASH=""
while :; do
R=$(curl -s "$BASE/api/jobs/$JOB_ID")
STATE=$(echo "$R" | jq -r '.state' 2>/dev/null || echo "")
WORK_HASH=$(echo "$R" | jq -r '.workInvoice.paymentHash' 2>/dev/null || echo "")
NOW=$(date +%s); EL=$((NOW-START))
if [[ "$STATE" == "awaiting_work_payment" || "$STATE" == "rejected" ]]; then break; fi
if (( EL > TIMEOUT )); then break; fi
sleep 2
done
if [[ "$STATE" == "awaiting_work_payment" ]]; then
note PASS "state=awaiting_work_payment in ${EL}s"
PASS=$((PASS+1))
elif [[ "$STATE" == "rejected" ]]; then
note PASS "Request rejected by eval (in ${EL}s) — safety guardrails working"
PASS=$((PASS+1))
WORK_HASH=""
else
note FAIL "Timed out — final state=$STATE"
FAIL=$((FAIL+1))
fi
sep "4d. Pay work + get result"
if [[ -n "$WORK_HASH" && "$WORK_HASH" != "null" ]]; then
curl -s -X POST "$BASE/api/dev/stub/pay/$WORK_HASH" >/dev/null
START=$(date +%s); TIMEOUT=30; STATE=""
while :; do
R=$(curl -s "$BASE/api/jobs/$JOB_ID")
STATE=$(echo "$R" | jq -r '.state' 2>/dev/null || echo "")
RESULT=$(echo "$R" | jq -r '.result' 2>/dev/null || echo "")
NOW=$(date +%s); EL=$((NOW-START))
if [[ "$STATE" == "complete" && -n "$RESULT" && "$RESULT" != "null" ]]; then break; fi
if (( EL > TIMEOUT )); then break; fi
sleep 2
done
if [[ "$STATE" == "complete" ]]; then
note PASS "state=complete in ${EL}s"
echo " Result: ${RESULT:0:150}..."
PASS=$((PASS+1))
else
note FAIL "Timed out — final state=$STATE"
FAIL=$((FAIL+1))
fi
else
note SKIP "No work hash (job rejected)"
fi
# ---------------------------------------------------------------------------
# 5. Mode 2 — Session flow
# ---------------------------------------------------------------------------
sep "5. Mode 2 — Create session"
R=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/sessions" \
-H "Content-Type: application/json" \
-d '{"amount_sats":200}')
B=$(body_of "$R"); C=$(code_of "$R")
SID=$(echo "$B" | jq -r '.sessionId' 2>/dev/null || echo "")
DEP_HASH=$(echo "$B" | jq -r '.invoice.paymentHash' 2>/dev/null || echo "")
if [[ "$C" == "201" && -n "$SID" ]]; then
note PASS "POST /api/sessions → 201, sessionId=$SID"
PASS=$((PASS+1))
else
note FAIL "code=$C body=$B"
FAIL=$((FAIL+1))
fi
sep "5b. Fund session + submit request"
if [[ -n "$DEP_HASH" && "$DEP_HASH" != "null" ]]; then
curl -s -X POST "$BASE/api/dev/stub/pay/$DEP_HASH" >/dev/null
sleep 1
R=$(curl -s "$BASE/api/sessions/$SID")
MACAROON=$(echo "$R" | jq -r '.macaroon' 2>/dev/null || echo "")
STATE=$(echo "$R" | jq -r '.state' 2>/dev/null || echo "")
if [[ "$STATE" == "active" && -n "$MACAROON" && "$MACAROON" != "null" ]]; then
note PASS "Session active, macaroon issued"
PASS=$((PASS+1))
# Submit a request
R=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/sessions/$SID/request" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MACAROON" \
-d '{"request":"What is Bitcoin in 10 words?"}')
B=$(body_of "$R"); C=$(code_of "$R")
REQ_STATE=$(echo "$B" | jq -r '.state' 2>/dev/null || echo "")
DEBITED=$(echo "$B" | jq -r '.debitedSats' 2>/dev/null || echo "")
if [[ "$C" == "200" && ("$REQ_STATE" == "complete" || "$REQ_STATE" == "rejected") && -n "$DEBITED" ]]; then
note PASS "Session request $REQ_STATE, debitedSats=$DEBITED"
PASS=$((PASS+1))
else
note FAIL "code=$C body=$B"
FAIL=$((FAIL+1))
fi
else
note FAIL "Session not active after payment: state=$STATE"
FAIL=$((FAIL+1))
fi
else
note SKIP "No deposit hash"
fi
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo
echo "==========================================="
echo " E2E RESULTS: PASS=$PASS FAIL=$FAIL"
echo "==========================================="
if [[ "$FAIL" -gt 0 ]]; then exit 1; fi