test(multiplayer): add disposable kind agones gate

This commit is contained in:
Josh Creek
2026-09-01 17:43:35 +01:00
parent e934bbfe44
commit 9b76d47c52
3 changed files with 110 additions and 3 deletions
+4 -1
View File
@@ -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
+2 -2
View File
@@ -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.398.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.478.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.17.8), live PostgreSQL/Redis execution where Docker is unavailable, live Agones/kind lifecycle (8.308.38, 8.49), public-network chaos/load/cost/release gates (8.508.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.17.8), live PostgreSQL/Redis execution where Docker is unavailable, live Agones/kind lifecycle (8.308.38, 8.49), public-network chaos/load/cost/release gates (8.508.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
+104
View File
@@ -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