#!/usr/bin/env bash
# test_e2e_connect.sh — Headless E2E connection test for hyper-tps.
#
# Uses the real installed app (hyper-tps → bare index.js → godot).
# Two-step flow:
# 1. Export config: discovers rooms, saves exact room_id to config file
# 2. Run test: two instances join the same room_id, poll diag files
#
# Usage:
# bash scripts/test_e2e_connect.sh # uses existing config
# bash scripts/test_e2e_connect.sh --export-config # re-export config, then test
# bash scripts/test_e2e_connect.sh --export-config-only # export config and exit
# bash scripts/test_e2e_connect.sh --room "The Lobby" # export config for this room
# bash scripts/test_e2e_connect.sh --config /path/to.json
# bash scripts/test_e2e_connect.sh --timeout 90
#
# Prerequisites:
# - hyper-tps installed via pgh app install
# - jq installed
# - keet-daemon running (hyper-tps starts it automatically)
set -euo pipefail
APP="hyper-tps"
CONFIG_DIR="$HOME/.config/hyper-tps"
CONFIG_PATH="$CONFIG_DIR/headless.json"
ROOM=""
TIMEOUT=60
EXPORT_CONFIG=false
EXPORT_ONLY=false
while [[ $# -gt 0 ]]; do
case $1 in
--config) CONFIG_PATH="$2"; shift 2 ;;
--room) ROOM="$2"; EXPORT_CONFIG=true; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--export-config) EXPORT_CONFIG=true; shift ;;
--export-config-only) EXPORT_CONFIG=true; EXPORT_ONLY=true; shift ;;
--help|-h)
echo "Usage: $0 [--config <path>] [--room <name>] [--timeout <secs>]"
echo " $0 --export-config # re-export config then run test"
echo " $0 --export-config-only # export config and exit"
echo " $0 --room \"The Lobby\" # export config for specific room"
exit 0 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
# Check deps
if ! command -v "$APP" &>/dev/null; then
echo "ERROR: $APP not found. Install with:"
echo " pgh app install pear://p9d1sizcqe4f8oepymqsp6o4bgbqs57aammb5sg716cirdh8aysy --name hyper-tps"
exit 1
fi
if ! command -v jq &>/dev/null; then
echo "ERROR: jq required. Install with: sudo apt install jq"
exit 1
fi
# ---- Step 1: Export config if needed ----
if [[ ! -f "$CONFIG_PATH" ]]; then
EXPORT_CONFIG=true
fi
if [[ "$EXPORT_CONFIG" == "true" ]]; then
echo "=== Exporting Config ==="
mkdir -p "$CONFIG_DIR"
EXPORT_ARGS=(--headless --export-config)
if [[ -n "$ROOM" ]]; then
EXPORT_ARGS+=(--room "$ROOM")
fi
echo "Running: $APP ${EXPORT_ARGS[*]}"
echo "Waiting for room discovery (this starts keet-daemon if needed)..."
"$APP" "${EXPORT_ARGS[@]}" 2>&1 | tee /tmp/hypertps-export.log
if [[ ! -f "$CONFIG_PATH" ]]; then
echo ""
echo "ERROR: Config was not created at $CONFIG_PATH"
echo "Check the log above — is the room name correct?"
exit 1
fi
echo ""
fi
# ---- Step 2: Confirm config ----
echo "=== Config ==="
jq '.' "$CONFIG_PATH"
echo ""
ROOM_ID=$(jq -r '.room_id' "$CONFIG_PATH")
ROOM_NAME=$(jq -r '.room_name' "$CONFIG_PATH")
ROLE=$(jq -r '.role // "peer"' "$CONFIG_PATH")
if [[ -z "$ROOM_ID" || "$ROOM_ID" == "null" ]]; then
echo "ERROR: No room_id in config. Re-export with: $0 --export-config --room <name>"
exit 1
fi
echo "Room: $ROOM_NAME"
echo "Room ID: $ROOM_ID"
echo ""
if [[ "$EXPORT_ONLY" == "true" ]]; then
echo "Config exported. Run test with: $0"
exit 0
fi
# ---- Step 3: Run E2E test ----
DIAG_A="/tmp/hypertps-e2e-A.diag.json"
DIAG_B="/tmp/hypertps-e2e-B.diag.json"
PID_A=""
PID_B=""
kill_all() {
[[ -n "$PID_A" ]] && kill -- -"$PID_A" 2>/dev/null || true
[[ -n "$PID_B" ]] && kill -- -"$PID_B" 2>/dev/null || true
sleep 1
[[ -n "$PID_A" ]] && kill -9 -- -"$PID_A" 2>/dev/null || true
[[ -n "$PID_B" ]] && kill -9 -- -"$PID_B" 2>/dev/null || true
PID_A=""; PID_B=""
}
cleanup() {
echo ""
echo "=== Cleaning up ==="
kill_all
}
trap cleanup EXIT
MAX_RETRIES=3
CONNECTED=false
for attempt in $(seq 1 "$MAX_RETRIES"); do
echo "=== E2E Connection Test (attempt $attempt/$MAX_RETRIES) ==="
echo "Timeout: ${TIMEOUT}s"
echo ""
rm -f "$DIAG_A" "$DIAG_B"
kill_all
# Start Instance A (admin) — --no-updates prevents OTA from corrupting files mid-test
echo "Starting Instance A (admin)..."
setsid "$APP" --no-updates --headless --config "$CONFIG_PATH" --role admin --diag-path "$DIAG_A" \
> /tmp/hypertps-e2e-A.log 2>&1 &
PID_A=$!
echo " pid=$PID_A log=/tmp/hypertps-e2e-A.log diag=$DIAG_A"
# Wait for A mesh_active
echo "Waiting for A mesh_active..."
MESH_UP=false
for i in $(seq 1 "$TIMEOUT"); do
if [[ -f "$DIAG_A" ]]; then
MESH_A=$(jq -r '.mesh_active // false' "$DIAG_A" 2>/dev/null || echo "false")
DIAG_ROOM=$(jq -r '.room_id // ""' "$DIAG_A" 2>/dev/null || echo "")
if [[ "$MESH_A" == "true" ]]; then
if [[ "$DIAG_ROOM" != "$ROOM_ID" && -n "$DIAG_ROOM" ]]; then
echo " WARNING: A joined room $DIAG_ROOM but expected $ROOM_ID"
fi
echo " A mesh active after ${i}s (room=$DIAG_ROOM)"
MESH_UP=true
break
fi
fi
if ! kill -0 "$PID_A" 2>/dev/null; then
# Check if OTA killed it
if grep -q "Update applied" /tmp/hypertps-e2e-A.log 2>/dev/null; then
echo " A died due to OTA update — will retry"
else
echo "ERROR: Instance A exited unexpectedly. Log:"
tail -20 /tmp/hypertps-e2e-A.log
exit 1
fi
break
fi
sleep 1
done
if [[ "$MESH_UP" != "true" ]]; then
if grep -q "Update applied" /tmp/hypertps-e2e-A.log 2>/dev/null; then
echo " OTA update applied during startup — retrying..."
echo ""
continue
fi
echo "ERROR: A did not become mesh active within ${TIMEOUT}s"
[[ -f "$DIAG_A" ]] && jq '.' "$DIAG_A"
echo "--- A log (last 30 lines) ---"
tail -30 /tmp/hypertps-e2e-A.log
exit 1
fi
# Start Instance B (peer) — --no-updates prevents OTA from corrupting files mid-test
echo ""
echo "Starting Instance B (peer)..."
setsid "$APP" --no-updates --headless --config "$CONFIG_PATH" --role peer --diag-path "$DIAG_B" \
> /tmp/hypertps-e2e-B.log 2>&1 &
PID_B=$!
echo " pid=$PID_B log=/tmp/hypertps-e2e-B.log diag=$DIAG_B"
# Poll both for actual game connectivity:
# - godot_peers: Godot multiplayer sees the other peer
# - players_spawned >= 2: both local + remote player nodes exist in world
echo ""
echo "Waiting for players to spawn (max ${TIMEOUT}s)..."
OTA_RETRY=false
for i in $(seq 1 "$TIMEOUT"); do
PEERS_A=0; PEERS_B=0; SPAWN_A=0; SPAWN_B=0
if [[ -f "$DIAG_A" ]]; then
PEERS_A=$(jq -r '.godot_peers | length' "$DIAG_A" 2>/dev/null || echo "0")
SPAWN_A=$(jq -r '.players_spawned // 0' "$DIAG_A" 2>/dev/null || echo "0")
fi
if [[ -f "$DIAG_B" ]]; then
PEERS_B=$(jq -r '.godot_peers | length' "$DIAG_B" 2>/dev/null || echo "0")
SPAWN_B=$(jq -r '.players_spawned // 0' "$DIAG_B" 2>/dev/null || echo "0")
fi
# Show connection states inline
STATES_A=""; STATES_B=""
[[ -f "$DIAG_A" ]] && STATES_A=$(jq -r '[.connections[].state] | join(",")' "$DIAG_A" 2>/dev/null || echo "")
[[ -f "$DIAG_B" ]] && STATES_B=$(jq -r '[.connections[].state] | join(",")' "$DIAG_B" 2>/dev/null || echo "")
echo " [${i}s] A: peers=$PEERS_A players=$SPAWN_A ${STATES_A:+[$STATES_A]} B: peers=$PEERS_B players=$SPAWN_B ${STATES_B:+[$STATES_B]}"
# Success: both see each other as Godot peers AND both have 2+ players spawned
if [[ "$PEERS_A" -gt 0 ]] && [[ "$PEERS_B" -gt 0 ]] && \
[[ "$SPAWN_A" -ge 2 ]] && [[ "$SPAWN_B" -ge 2 ]]; then
CONNECTED=true
break
fi
# Check if either died
if ! kill -0 "$PID_A" 2>/dev/null; then
if grep -q "Update applied" /tmp/hypertps-e2e-A.log 2>/dev/null; then
echo " A died due to OTA update — will retry"
OTA_RETRY=true
else
echo "ERROR: A died"
fi
break
fi
if ! kill -0 "$PID_B" 2>/dev/null; then
if grep -q "Update applied" /tmp/hypertps-e2e-B.log 2>/dev/null || \
grep -q "Failed loading resource" /tmp/hypertps-e2e-B.log 2>/dev/null; then
echo " B died due to OTA update corrupting files — will retry"
OTA_RETRY=true
else
echo "ERROR: B died"
fi
break
fi
sleep 1
done
if [[ "$CONNECTED" == "true" ]]; then
break
fi
if [[ "$OTA_RETRY" == "true" && "$attempt" -lt "$MAX_RETRIES" ]]; then
echo ""
echo "OTA update interfered — restarting both instances..."
echo ""
continue
fi
# Not OTA, or out of retries — fall through to results
break
done
echo ""
echo "=== Results ==="
if [[ "$CONNECTED" == "true" ]]; then
echo "SUCCESS: Both players spawned in room '$ROOM_NAME' ($ROOM_ID)"
echo ""
for label in A B; do
DIAG="/tmp/hypertps-e2e-${label}.diag.json"
echo "--- Instance $label ---"
if [[ -f "$DIAG" ]]; then
jq '{godot_peers, players_spawned, swarm, dht}' "$DIAG"
fi
echo ""
done
exit 0
fi
echo "FAILURE: Players did not spawn within ${TIMEOUT}s"
echo ""
for label in A B; do
DIAG="/tmp/hypertps-e2e-${label}.diag.json"
LOG="/tmp/hypertps-e2e-${label}.log"
echo "--- Instance $label ---"
if [[ -f "$DIAG" ]]; then
jq '.' "$DIAG"
echo ""
echo "Connections:"
jq -r '.connections[] | " #\(.id) \(.state) init=\(.is_initiator) relay=\(.relay) server=\(.server) punch=\(.punched) addr=\(.punched_address) fw=\(.firewall_local)/\(.firewall_remote) elapsed=\(.elapsed_ms)ms addrs=\(.remote_addresses) err=\(.error)"' "$DIAG" 2>/dev/null || echo " (none)"
else
echo "(no diag file)"
fi
echo ""
echo "Log (last 20 lines):"
tail -20 "$LOG" 2>/dev/null || echo "(no log)"
echo ""
done
exit 1