OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / distribute / hypertps-win64 / scripts / test_e2e_connect.sh

313 lines · bash ref:
#!/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