239 lines
8.6 KiB
Bash
239 lines
8.6 KiB
Bash
#!/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 '<title>[^<]*</title>' | 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
|