mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
521b8122ac
Every existing test of the client/control-plane boundary is either a Go unit test with a mocked HTTP layer or a GDScript unit test with no network at all (multiplayer-next.md 8.40's own evidence names "live multi-process control-plane/game verification" as remaining). Nothing before this actually ran the real compiled Go binary, a real PostgreSQL instance, and a real headless Godot process talking real HTTP to each other -- and it immediately found a real bug (previous commit). server/cmd/testkit-api is a new, deliberately separate, clearly-marked test-only binary wired identically to cmd/control-plane except for SteamLogin: cmd/control-plane has no way to authenticate against a real Steam Web API from this sandbox (task 8.7's own documented blocker), so testkit-api accepts any non-empty ticket string and derives a deterministic identity instead. This bypass is confined to its own binary -- never a flag on cmd/control-plane, never referenced by any Dockerfile stage or Kubernetes manifest -- specifically so it can't become a footgun on the real one. Game/tests/control_plane_smoke.gd drives the real ControlPlaneClient autoload through login -> queue_create -> heartbeat against a real server and prints SMOKE PASS/FAIL, matching the existing net_smoke.gd convention. scripts/verify_control_plane_integration.sh orchestrates both sides (real postgres:17-alpine, the built testkit-api binary, the Godot client) end to end. Two real bugs surfaced building this, both fixed and re-verified, not just the target bug: the smoke script's own use of `go run` left a zombie process that survived cleanup and squatting on its port corrupted the NEXT run with a misleading "http=401 unauthorized" (now builds and runs a real binary directly, plus a belt-and-suspenders port-kill in cleanup); and calling heartbeat() synchronously from within a request_succeeded handler produced a spurious "Busy" because ControlPlaneClient's own internal resync (see previous commit) was still in flight -- the test now waits for ControlPlaneClient to go idle via a real Timer (call_deferred alone floods the message queue without ever yielding a frame for the in-flight request to complete). Verified stable across 3 consecutive full runs: real PostgreSQL container up, migrations applied, testkit-api built and started, real headless Godot client round-tripping login/queue/heartbeat, clean teardown with no leftover processes, containers, or bound ports each time.
107 lines
4.1 KiB
Bash
Executable File
107 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Real end-to-end verification: a real PostgreSQL instance, the real Go
|
|
# api.Service wired exactly like cmd/control-plane (except for auth -- see
|
|
# below), and a real headless Godot process driving ControlPlaneClient over
|
|
# an actual network connection. Every other test of this boundary is either
|
|
# a Go unit test with a mocked HTTP layer or a GDScript unit test with no
|
|
# network at all; this is the one place that proves the wire format the two
|
|
# languages actually agree on, not just that each side's own tests pass.
|
|
#
|
|
# Uses server/cmd/testkit-api rather than the real cmd/control-plane binary:
|
|
# that binary has no way to authenticate a Steam Web API ticket without a
|
|
# real Steam backend, which this sandbox cannot provide (see
|
|
# multiplayer-next.md task 8.7). testkit-api is wired identically otherwise
|
|
# and is never referenced by any Dockerfile stage or Kubernetes manifest --
|
|
# see its own file header for why that bypass is confined to a distinctly
|
|
# named, obviously-not-production binary rather than a flag on the real one.
|
|
|
|
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$root_dir"
|
|
|
|
godot_bin="${GODOT_BIN:-godot}"
|
|
container_name="cosmic-clash-control-plane-integration"
|
|
database="cosmic_clash_test"
|
|
user="cosmic_clash_test"
|
|
password="cosmic_clash_test"
|
|
pg_port="55434"
|
|
api_port="18099"
|
|
logs_dir="$(mktemp -d "${TMPDIR:-/tmp}/cosmic-clash-control-plane.XXXXXX")"
|
|
|
|
testkit_pid=""
|
|
cleanup() {
|
|
local status=$?
|
|
if (( status != 0 )); then
|
|
for log_file in "$logs_dir"/*.log; do
|
|
[[ -f "$log_file" ]] || continue
|
|
echo "--- $log_file" >&2
|
|
cat "$log_file" >&2
|
|
done
|
|
fi
|
|
[[ -n "$testkit_pid" ]] && kill "$testkit_pid" 2>/dev/null || true
|
|
# Belt-and-suspenders after the go-run zombie above: make sure nothing is
|
|
# left listening on this run's own port before the trap exits.
|
|
lsof -ti "tcp:${api_port}" 2>/dev/null | xargs -r kill -9 2>/dev/null || true
|
|
docker rm -f "$container_name" >/dev/null 2>&1 || true
|
|
echo "Control-plane integration logs: $logs_dir"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
docker rm -f "$container_name" >/dev/null 2>&1 || true
|
|
docker run --rm -d --name "$container_name" \
|
|
-e POSTGRES_DB="$database" \
|
|
-e POSTGRES_USER="$user" \
|
|
-e POSTGRES_PASSWORD="$password" \
|
|
-p "${pg_port}:5432" postgres:17-alpine >/dev/null
|
|
|
|
for attempt in $(seq 1 30); do
|
|
if docker exec "$container_name" pg_isready -U "$user" -d "$database" >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
if [ "$attempt" = 30 ]; then
|
|
echo "PostgreSQL did not become ready" >&2
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
dsn="postgres://${user}:${password}@127.0.0.1:${pg_port}/${database}?sslmode=disable"
|
|
|
|
# `go run` wraps the real binary in a build/exec parent whose own PID does
|
|
# not reliably propagate a `kill` to the child it spawns -- confirmed the
|
|
# hard way: a prior run's leftover process survived cleanup, kept squatting
|
|
# on this exact port bound to an already-torn-down PostgreSQL container, and
|
|
# silently intercepted the NEXT run's connection, turning a real login into
|
|
# an "http=401 unauthorized" failure with no indication the server it
|
|
# actually reached was a zombie from a previous run. Build once and run the
|
|
# real binary directly so its own PID is what gets killed.
|
|
go -C server build -o "$logs_dir/testkit-api" ./cmd/testkit-api
|
|
COSMIC_CLASH_POSTGRES_DSN="$dsn" "$logs_dir/testkit-api" --listen="127.0.0.1:${api_port}" --migrations="$root_dir/server/migrations" \
|
|
>"$logs_dir/testkit-api.log" 2>&1 &
|
|
testkit_pid=$!
|
|
|
|
for attempt in $(seq 1 30); do
|
|
if curl -sSf "http://127.0.0.1:${api_port}/healthz" >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
if [ "$attempt" = 30 ]; then
|
|
echo "testkit-api did not become ready" >&2
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
"$godot_bin" --headless --path Game res://tests/control_plane_smoke.tscn -- \
|
|
--control-plane-url="http://127.0.0.1:${api_port}" \
|
|
>"$logs_dir/godot-client.log" 2>&1
|
|
status=$?
|
|
|
|
if [ "$status" -ne 0 ] || ! grep -q "^SMOKE PASS:" "$logs_dir/godot-client.log"; then
|
|
echo "Control-plane integration FAILED (exit $status)" >&2
|
|
cat "$logs_dir/godot-client.log" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Control-plane integration PASS"
|