#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" API_PORT="${MATCHDAY_E2E_API_PORT:-8180}" WEB_PORT="${MATCHDAY_E2E_WEB_PORT:-3100}" TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/matchday-v3-e2e.XXXXXX")" API_LOG="$TMP_DIR/api.log" WEB_LOG="$TMP_DIR/web.log" NEXT_DIST_DIR=".next-e2e" NEXT_ENV_BACKUP="$TMP_DIR/next-env.d.ts" cp "$ROOT/next-env.d.ts" "$NEXT_ENV_BACKUP" terminate_group() { local pid="${1:-}" if [[ -z "$pid" ]]; then return fi kill -TERM -- "-$pid" 2>/dev/null || true for _ in $(seq 1 30); do if ! kill -0 "$pid" 2>/dev/null; then break fi sleep 0.1 done if kill -0 "$pid" 2>/dev/null; then kill -KILL -- "-$pid" 2>/dev/null || true fi wait "$pid" 2>/dev/null || true } cleanup() { local status=$? terminate_group "${API_PID:-}" terminate_group "${WEB_PID:-}" rm -rf "$ROOT/$NEXT_DIST_DIR" cp "$NEXT_ENV_BACKUP" "$ROOT/next-env.d.ts" if [[ "$status" -eq 0 ]]; then rm -rf "$TMP_DIR" elif [[ -s "$API_LOG" || -s "$WEB_LOG" ]]; then printf 'E2E failed; logs preserved at %s\n' "$TMP_DIR" >&2 else rm -rf "$TMP_DIR" fi return "$status" } trap cleanup EXIT INT TERM assert_port_free() { local label="$1" local port="$2" if ! python3 - "$port" <<'PY' import socket import sys port = int(sys.argv[1]) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: try: listener.bind(("127.0.0.1", port)) except OSError as exc: print(f"port {port} is unavailable: {exc}", file=sys.stderr) raise SystemExit(1) from exc PY then printf 'E2E %s port %s is already in use; refusing to test an unrelated process.\n' "$label" "$port" >&2 exit 1 fi } assert_port_free "API" "$API_PORT" assert_port_free "web" "$WEB_PORT" cd "$ROOT" export MATCHDAY_ENV=test export MATCHDAY_QWEN_MODE=scripted # Keep regression E2E deterministic; live provider behavior has a separate contract test/smoke. export SERPAPI_API_KEY="" export MATCHDAY_BACKEND_SIGNING_SECRET=matchday-e2e-local-signing-secret export MATCHDAY_AUTH_NONCE_STORE=memory export MATCHDAY_AGENT_API_URL="http://127.0.0.1:$API_PORT" export DATABASE_URL="sqlite:///$TMP_DIR/matchday.db" export MATCHDAY_RUN_LIMIT_PER_USER_HOUR=100 export MATCHDAY_RUN_LIMIT_GLOBAL_HOUR=200 # Keep relative "upcoming" fixture journeys stable as real tournament dates pass. # The backend rejects this clock outside MATCHDAY_ENV=test. export MATCHDAY_FIXTURE_REFERENCE_TIME="2026-07-12T12:00:00Z" export NEXT_TELEMETRY_DISABLED=1 export NEXT_DIST_DIR export PYTHONPATH=backend python3 -c 'import os, sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \ python3 -m uvicorn matchday.main:app \ --host 127.0.0.1 --port "$API_PORT" >"$API_LOG" 2>&1 & API_PID=$! python3 -c 'import os, sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \ node "$ROOT/node_modules/next/dist/bin/next" dev \ --hostname 127.0.0.1 --port "$WEB_PORT" >"$WEB_LOG" 2>&1 & WEB_PID=$! for _ in $(seq 1 60); do if ! kill -0 "$API_PID" 2>/dev/null; then printf 'E2E API process exited before readiness (port %s may already be in use).\n' "$API_PORT" >&2 cat "$API_LOG" >&2 exit 1 fi if ! kill -0 "$WEB_PID" 2>/dev/null; then printf 'E2E web process exited before readiness (port %s may already be in use).\n' "$WEB_PORT" >&2 cat "$WEB_LOG" >&2 exit 1 fi if curl -fsS "http://127.0.0.1:$API_PORT/healthz" >/dev/null 2>&1 && \ curl -fsS "http://127.0.0.1:$WEB_PORT" >/dev/null 2>&1; then break fi sleep 0.5 done if ! kill -0 "$API_PID" 2>/dev/null; then printf 'E2E API process is not the process serving port %s.\n' "$API_PORT" >&2 cat "$API_LOG" >&2 exit 1 fi if ! kill -0 "$WEB_PID" 2>/dev/null; then printf 'E2E web process is not the process serving port %s.\n' "$WEB_PORT" >&2 cat "$WEB_LOG" >&2 exit 1 fi if ! curl -fsS "http://127.0.0.1:$API_PORT/healthz" >/dev/null 2>&1; then cat "$API_LOG" exit 1 fi if ! curl -fsS "http://127.0.0.1:$WEB_PORT" >/dev/null 2>&1; then cat "$WEB_LOG" exit 1 fi MATCHDAY_E2E_BASE_URL="http://127.0.0.1:$WEB_PORT" \ python3 -m pytest e2e -q "$@"