From 9b76d47c521f233626951232d24b71ce5bf12dea Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:43:35 +0100 Subject: [PATCH] test(multiplayer): add disposable kind agones gate --- Makefile | 5 +- multiplayer-next.md | 4 +- scripts/verify_kind_agones.sh | 104 ++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100755 scripts/verify_kind_agones.sh diff --git a/Makefile b/Makefile index a11a8552..176093ac 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: verify-phase6 verify-enet-integration verify-steam-templates verify-supply-chain verify-multiplayer-local +.PHONY: verify-phase6 verify-enet-integration verify-steam-templates verify-supply-chain verify-kind-agones verify-multiplayer-local verify-multiplayer-local: bash scripts/verify_multiplayer_local.sh @@ -14,3 +14,6 @@ verify-steam-templates: verify-supply-chain: python3 scripts/verify_supply_chain.py + +verify-kind-agones: + bash scripts/verify_kind_agones.sh diff --git a/multiplayer-next.md b/multiplayer-next.md index a2f3dca2..fd9af9e9 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -1248,7 +1248,7 @@ the local/CI/community transport, not a silent production fallback. | 8.46 `[D:8.5,8.7,8.9,8.10,8.14,8.18,8.21,8.23,8.25]` | **IN PROGRESS.** Go unit/race coverage spans the current domain/store/supervisor policies, and fuzz targets now exercise queue input, result payload hashing and revision events | `server/domain/*_test.go`, `server/store/*_test.go`, `server/supervisor/*_test.go`, `server/migrations/*_test.go` and `server/domain/fuzz_test.go` pass normal/race suites; `go test -race ./...` passes across API, domain, migrations, observability, store, supervisor and testkit; `go vet ./...` passes; each of the three declared domain fuzz targets passes a bounded 4-second run; PostgreSQL live migration execution now runs clean (§8.5), and four real-concurrency cases are covered against a live database with `-race`: §8.14's queue-heartbeat revision race, §8.18's two-matcher contested-ticket race, §8.30's cross-allocator-replica capacity race, and §8.21/§8.25's concurrent identical-result-submission race; the "lost Redis" fixture is covered live against a real server (§8.14: real TTL expiry, repair-after-`FLUSHALL`; fake Steam/allocator fixtures are §8.47's testkit, already done). Further transaction fixtures (e.g. concurrent proposal-recovery expiry races, live Redis failover mid-write under load) remain | | 8.47 `[D:8.7,8.30]` | **IN PROGRESS.** Offline testkit provides deterministic fake Steam verification and fake allocation with forced failure injection | `server/testkit/` covers verified identity/replay, unknown identity, wrong App ID, expiry, no capacity, compatibility-key conflict, idempotent allocation replay and cloud-free forced allocation failure in `TestOfflineFakesCoverVerificationAndAllocationFailureMatrix`; API/Compose integration and live exhaustive matrix remain | | 8.48 `[D:8.10,8.14,8.17,8.18,8.27,8.31,8.35,8.47]` | **IN PROGRESS.** Offline testkit exercises verified queue projection → ranked six-player proposal → ENet allocation → assignment-ready manifest → certified durable result receipt | `server/testkit/pipeline_test.go` covers the cross-domain success path without Steam/cloud secrets; independent Compose fixture, process shutdown, result ack over HTTP and legacy fixture non-regression remain | -| 8.49 `[D:8.25,8.26,8.28,8.29,8.30,8.31,8.35,8.36]` | Disposable `kind` + Agones integration gate | CI covers dynamic ports, both readiness stages, roster/no-show, races, multi-match node, result-pending reconciliation, drain and rollback | +| 8.49 `[D:8.25,8.26,8.28,8.29,8.30,8.31,8.35,8.36]` | **IN PROGRESS.** `scripts/verify_kind_agones.sh` creates a disposable kind cluster, installs pinned Agones, loads the real `game-server` image, applies the Fleet, and verifies readiness plus allocation of a dynamic UDP endpoint | The cloud-free runner is now committed and fails clearly when Docker/kind/Helm are unavailable. CI/live evidence for both readiness stages, roster/no-show, races, multi-match node, result-pending reconciliation, drain, and rollback remains open | | 8.50 `[D:8.25,8.37,8.43,8.49]` | Network/chaos suite: 100 ms RTT, jitter/loss, client/API/matcher restart, game-pod death, node drain, Redis failover and control-plane loss | System recovers to a defined state; infrastructure-caused cases cannot penalise affected players | | 8.51 `[D:8.17,8.18,8.30,8.31,8.45]` | Load test >=10,000 queued clients, >=100 proposals/s and forecast launch concurrency x2 | API p95 <=250 ms, durable matcher fence holds, both readiness/allocation SLOs are met and replicas scale without duplicate claims | | 8.52 `[D:8.32,8.34,8.45,8.51]` | Per-region cost model from measured density, warm capacity, bandwidth, DB/Redis and telemetry; add budgets and allocation quotas | Cost per completed match and forecast monthly bands are recorded; a denial-of-wallet test triggers limits/alerts before budget breach | @@ -1455,7 +1455,7 @@ Observability redaction now adds content-aware protection on top of denylisted f The following Phase 8 slices have local implementation and verification evidence in this document: 8.29 dynamic allocated launch flags and endpoint handling; 8.30 allocator claim/reconciliation; 8.31 signed assignment/roster validation; 8.35 initial-connect no-show and casual bot policy; 8.36 controlled drain and shutdown acknowledgment; 8.39–8.43 client state, assignment, profile, recovery, and idempotent action retry; 8.44 structured observability and content-aware redaction; 8.45 bounded API metrics export plus optional Prometheus scrape/alert rules; 8.46 normal/race/vet/fuzz coverage; and 8.47–8.48 offline testkit coverage. Their remaining acceptance text is infrastructure or production dependent where explicitly noted below the corresponding row. -The following are not locally certifiable from this workspace and remain open prerequisites rather than silently “done”: Valve/GodotSteam credentials and hosted SDR (7.1–7.8), live PostgreSQL/Redis execution where Docker is unavailable, live Agones/kind lifecycle (8.30–8.38, 8.49), public-network chaos/load/cost/release gates (8.50–8.53), and real-hardware graphics profiling (0.15b onward). `TODO.md`’s AI-training and presentation tasks remain separate from multiplayer and are not marked by this index. +The following are not locally certifiable from this workspace and remain open prerequisites rather than silently “done”: Valve/GodotSteam credentials and hosted SDR (7.1–7.8), live PostgreSQL/Redis execution where Docker is unavailable, live Agones/kind lifecycle (8.30–8.38, 8.49), public-network chaos/load/cost/release gates (8.50–8.53), and real-hardware graphics profiling (0.15b onward). `make verify-kind-agones` is the committed runner for 8.49; it requires a running Docker daemon plus kind, kubectl, and Helm. `TODO.md`’s AI-training and presentation tasks remain separate from multiplayer and are not marked by this index. The deferred teamplay TODO prerequisite is now implemented locally but not enabled: team-touch credit is opt-in and the evaluator can run paired 2v2 diff --git a/scripts/verify_kind_agones.sh b/scripts/verify_kind_agones.sh new file mode 100755 index 00000000..8a4cde93 --- /dev/null +++ b/scripts/verify_kind_agones.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Disposable integration gate for multiplayer-next.md §8.49. This deliberately +# does not touch an existing cluster: kind creates an isolated cluster and the +# EXIT trap removes only that named cluster. +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$root_dir" + +cluster_name="${KIND_CLUSTER_NAME:-cosmic-clash-agones-smoke}" +agones_version="${AGONES_VERSION:-1.49.0}" +game_server_image="${GAME_SERVER_IMAGE:-cosmic-clash-game-server:kind}" +kind_node_image="${KIND_NODE_IMAGE:-kindest/node:v1.33.1}" +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/cosmic-clash-agones.XXXXXX")" + +cleanup() { + local status=$? + kind delete cluster --name "$cluster_name" >/dev/null 2>&1 || true + rm -rf "$work_dir" + exit "$status" +} +trap cleanup EXIT + +for tool in docker kind kubectl helm; do + command -v "$tool" >/dev/null 2>&1 || { + echo "8.49 requires '$tool'; install Docker, kind, kubectl, and Helm to run the disposable gate" >&2 + exit 2 + } +done + +if ! docker info >/dev/null 2>&1; then + echo "8.49 requires a running Docker daemon" >&2 + exit 2 +fi + +kind delete cluster --name "$cluster_name" >/dev/null 2>&1 || true + +if ! docker image inspect "$game_server_image" >/dev/null 2>&1; then + echo "Building $game_server_image from the pinned game-server target" + docker build --target game-server -t "$game_server_image" . +fi + +kind create cluster --name "$cluster_name" --image "$kind_node_image" --wait 120s +kind load docker-image "$game_server_image" --name "$cluster_name" + +helm repo add agones https://agones.dev/chart/stable >/dev/null +helm repo update >/dev/null +helm upgrade --install agones agones/agones \ + --namespace agones-system --create-namespace \ + --version "$agones_version" \ + --set agones.crds.cleanup.enabled=true \ + --set agones.controller.replicas=1 \ + --set agones.extensions.replicas=1 \ + --set agones.allocator.replicas=1 \ + --wait --timeout 5m + +kubectl wait --for=condition=available deployment/agones-controller \ + -n agones-system --timeout=180s +kubectl wait --for=condition=available deployment/agones-allocator \ + -n agones-system --timeout=180s + +# The base Fleet intentionally carries a release-time digest placeholder. For +# this isolated run only, replace that exact placeholder with the image loaded +# into kind. No repository manifest is modified and no mutable image is used +# outside the disposable cluster. +sed "s|ghcr.io/cosmic-clash/game-server@sha256:$(printf '0%.0s' {1..64})|$game_server_image|" \ + deploy/k8s/base/fleet.yaml > "$work_dir/fleet.yaml" + +kubectl apply -f deploy/k8s/base/namespace.yaml +kubectl -n cosmic-clash create secret generic cosmic-clash-game-server \ + --from-literal=drain-token=kind-smoke-drain-token \ + --from-literal=join-signing-key=kind-smoke-signing-key \ + --dry-run=client -o yaml | kubectl apply -f - +kubectl apply -f deploy/k8s/base/service-accounts.yaml +kubectl apply -f "$work_dir/fleet.yaml" + +kubectl wait --for=jsonpath='{.status.ready}'=2 \ + fleet/cosmic-clash-game -n cosmic-clash --timeout=5m + +cat > "$work_dir/allocation.yaml" <<'EOF' +apiVersion: allocation.agones.dev/v1 +kind: GameServerAllocation +metadata: + generateName: cosmic-clash-smoke- + namespace: cosmic-clash +spec: + fleet: + name: cosmic-clash-game +EOF +kubectl create -f "$work_dir/allocation.yaml" -o json > "$work_dir/allocation.json" + +python3 - "$work_dir/allocation.json" <<'PY' +import json +import sys + +doc = json.load(open(sys.argv[1], encoding="utf-8")) +status = doc.get("status", {}) +if status.get("state") != "Allocated": + raise SystemExit(f"allocation state is {status.get('state')!r}, expected 'Allocated'") +ports = status.get("gameServer", {}).get("status", {}).get("ports", []) +if not ports or not any(p.get("port", 0) > 0 and p.get("port") != 7777 for p in ports): + raise SystemExit(f"allocation did not return a dynamic UDP port: {ports!r}") +print("8.49 PASS: Fleet became ready and allocation returned a dynamic UDP port") +PY