test(multiplayer): add allocated compose smoke flow

This commit is contained in:
Josh Creek
2026-09-01 17:50:52 +01:00
parent 8129bb2571
commit 88eb510dc3
8 changed files with 188 additions and 3 deletions
+27
View File
@@ -0,0 +1,27 @@
name: Allocated Compose Smoke
on:
workflow_dispatch:
pull_request:
paths:
- Dockerfile
- Makefile
- compose.allocated-smoke.yml
- server/api/**
- server/store/**
- server/workload/**
- server/migrations/**
- scripts/verify_allocated_compose.sh
- .github/workflows/allocated-compose.yml
permissions:
contents: read
jobs:
allocated-compose:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Verify independent allocated Compose flow
run: make verify-allocated-compose
+7
View File
@@ -57,6 +57,7 @@ COPY server/go.mod server/go.sum ./
RUN go mod download
COPY server/ ./
RUN CGO_ENABLED=0 go build -o /opt/cosmic-clash/game-server-supervisor ./cmd/game-server-supervisor
RUN CGO_ENABLED=0 go build -o /opt/cosmic-clash/testkit-api ./cmd/testkit-api
# Agones-allocated fleet image: the same dedicated-server export as `server`
# (unchanged above; make verify-phase6 exercises that target exactly as
@@ -71,3 +72,9 @@ FROM server AS game-server
COPY --from=supervisor-build /opt/cosmic-clash/game-server-supervisor /opt/cosmic-clash/game-server-supervisor
RUN chmod 0755 /opt/cosmic-clash/game-server-supervisor
ENTRYPOINT ["/opt/cosmic-clash/game-server-supervisor"]
FROM server AS testkit-api
COPY --from=supervisor-build /opt/cosmic-clash/testkit-api /opt/cosmic-clash/testkit-api
COPY server/migrations /opt/cosmic-clash/migrations
RUN chmod 0755 /opt/cosmic-clash/testkit-api
ENTRYPOINT ["/opt/cosmic-clash/testkit-api"]
+4 -1
View File
@@ -1,4 +1,4 @@
.PHONY: verify-phase6 verify-enet-integration verify-steam-templates verify-supply-chain verify-kind-agones verify-multiplayer-local
.PHONY: verify-phase6 verify-enet-integration verify-steam-templates verify-supply-chain verify-kind-agones verify-allocated-compose verify-multiplayer-local
verify-multiplayer-local:
bash scripts/verify_multiplayer_local.sh
@@ -17,3 +17,6 @@ verify-supply-chain:
verify-kind-agones:
bash scripts/verify_kind_agones.sh
verify-allocated-compose:
bash scripts/verify_allocated_compose.sh
+26
View File
@@ -0,0 +1,26 @@
services:
database:
image: postgres:17-alpine
environment:
POSTGRES_DB: cosmic_clash_test
POSTGRES_USER: cosmic_clash_test
POSTGRES_PASSWORD: cosmic_clash_test
healthcheck:
test: ["CMD-SHELL", "pg_isready -U cosmic_clash_test -d cosmic_clash_test"]
interval: 1s
timeout: 3s
retries: 30
control-plane:
build:
context: .
target: testkit-api
environment:
COSMIC_CLASH_POSTGRES_DSN: postgres://cosmic_clash_test:cosmic_clash_test@database:5432/cosmic_clash_test?sslmode=disable
COSMIC_CLASH_WORKLOAD_SECRET: compose-workload-secret
command: ["--listen=0.0.0.0:8080", "--migrations=/opt/cosmic-clash/migrations"]
depends_on:
database:
condition: service_healthy
ports:
- "18080:8080"
+2 -2
View File
@@ -1247,7 +1247,7 @@ the local/CI/community transport, not a silent production fallback.
| 8.45 `[D:8.2,8.44]` | **LOCAL COMPLETE; PRODUCTION GATE OPEN.** Go observability package turns the documented RTT, allocation/connect latency, result-success, API-latency and tick/headroom thresholds into executable window checks; the API exporter emits a bounded cumulative latency histogram suitable for querying the documented p95 API SLO | `server/observability/slo.go`, `metrics.go`, `deploy/observability/prometheus-rules.yaml` and adversarial tests cover healthy/violating/empty windows, fixed operation/status labels, cumulative bucket boundaries, arbitrary-path cardinality safety, and optional API p95/5xx alerts. Production scrape configuration, alert routing, wait/MMR/proposal/flood/cost series and runbooks remain |
| 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.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; `compose.allocated-smoke.yml` independently runs the real testkit API against PostgreSQL and verifies authenticated result, idempotent retry, shutdown acknowledgment, durable receipt/audit rows, and clean API process stop | `.github/workflows/allocated-compose.yml` runs `make verify-allocated-compose`. Allocated game-process drain, full queue/proposal/allocation orchestration over HTTP, and legacy fixture non-regression remain open |
| 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; `.github/workflows/agones-integration.yml` runs it for infrastructure changes and on demand | The cloud-free runner is 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 |
@@ -1460,7 +1460,7 @@ Observability redaction now adds content-aware protection on top of denylisted f
### Current local completion index (2026-09-01)
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 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/Compose 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). `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.
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -euo pipefail
# Independent allocated-flow fixture for multiplayer-next.md §8.48. This
# intentionally does not call compose.phase6-smoke.yml or reuse its ports.
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
compose_file="$root_dir/compose.allocated-smoke.yml"
project="${COMPOSE_PROJECT_NAME:-cosmic-clash-allocated-smoke}"
api_url="http://127.0.0.1:18080"
secret="compose-workload-secret"
compose=(docker compose -p "$project" -f "$compose_file")
cleanup() {
local rc=$?
"${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true
exit "$rc"
}
trap cleanup EXIT
command -v docker >/dev/null 2>&1 || { echo "Docker is required for 8.48" >&2; exit 2; }
docker info >/dev/null 2>&1 || { echo "A running Docker daemon is required for 8.48" >&2; exit 2; }
"${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true
"${compose[@]}" up -d --build
for attempt in $(seq 1 60); do
if curl -fsS "$api_url/healthz" >/dev/null 2>&1; then
break
fi
if [[ "$attempt" == 60 ]]; then
"${compose[@]}" logs >&2
echo "allocated Compose control plane did not become ready" >&2
exit 1
fi
sleep 1
done
# Model the durable state produced by the allocator, then use the real HTTP
# workload authentication and mutation boundaries for every action below.
"${compose[@]}" exec -T database psql -v ON_ERROR_STOP=1 -U cosmic_clash_test -d cosmic_clash_test <<'SQL'
INSERT INTO game_servers (server_id, region, build, protocol_version, transport, state)
VALUES ('compose-server', 'EU', 'build-1', 1, 'enet', 'ALLOCATED');
INSERT INTO matches (match_id, playlist, state, region, protocol_version, server_id, revision)
VALUES ('compose-match', 'casual', 'RESULT_PENDING', 'EU', 1, 'compose-server', 2);
INSERT INTO allocations (allocation_id, match_id, server_id, region, build, protocol_version, transport, request_digest, state, allocated_at)
VALUES ('compose-allocation', 'compose-match', 'compose-server', 'EU', 'build-1', 1, 'enet', decode(repeat('00', 32), 'hex'), 'ALLOCATED', now());
SQL
token="$(python3 - "$secret" <<'PY'
import base64, hashlib, hmac, json, sys, time
secret = sys.argv[1].encode()
payload = {"a": "compose-allocation", "e": time.time() + 300}
encoded = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode()).rstrip(b"=")
signature = hmac.new(secret, encoded, hashlib.sha256).digest()
sig = base64.urlsafe_b64encode(signature).rstrip(b"=")
print(encoded.decode() + "." + sig.decode())
PY
)"
result_body='{"match_id":"compose-match","result_nonce":"compose-result-nonce-1234","score":{"team_0":3,"team_1":2},"integrity_state":"CERTIFIED"}'
curl -fsS -o /dev/null -w '%{http_code}' \
-X POST "$api_url/v1/servers/compose-server/result" \
-H "Authorization: Bearer $token" \
-H 'Idempotency-Key: compose-result-key-123456' \
-H 'Content-Type: application/json' -d "$result_body" | grep -qx 202
# An identical retry must be acknowledged without a second receipt.
curl -fsS -o /dev/null -w '%{http_code}' \
-X POST "$api_url/v1/servers/compose-server/result" \
-H "Authorization: Bearer $token" \
-H 'Idempotency-Key: compose-result-key-123456' \
-H 'Content-Type: application/json' -d "$result_body" | grep -qx 202
"${compose[@]}" exec -T database psql -At -U cosmic_clash_test -d cosmic_clash_test -c "SELECT state FROM matches WHERE match_id = 'compose-match'" | grep -qx COMPLETED
"${compose[@]}" exec -T database psql -At -U cosmic_clash_test -d cosmic_clash_test -c "SELECT count(*) FROM result_receipts WHERE match_id = 'compose-match'" | grep -qx 1
curl -fsS -o /dev/null -w '%{http_code}' \
-X POST "$api_url/v1/servers/compose-server/shutdown" \
-H "Authorization: Bearer $token" \
-H 'Idempotency-Key: compose-shutdown-key-123456' \
-H 'Content-Type: application/json' -d '{"reason":"server_draining"}' | grep -qx 204
"${compose[@]}" exec -T database psql -At -U cosmic_clash_test -d cosmic_clash_test -c "SELECT count(*) FROM audit_events WHERE action = 'SERVER_SHUTDOWN' AND aggregate_id = 'compose-match'" | grep -qx 1
"${compose[@]}" stop -t 10 control-plane >/dev/null
if "${compose[@]}" ps --status running --services | grep -qx control-plane; then
echo "control-plane did not stop cleanly" >&2
exit 1
fi
echo "8.48 PASS: allocated Compose HTTP result/retry/shutdown flow completed"
+1
View File
@@ -28,6 +28,7 @@ echo "local multiplayer gate: contracts and manifests"
python3 -m json.tool "$root_dir/server/contracts/v1/openapi.json" >/dev/null
python3 "$root_dir/server/migrations/test_migration.py"
python3 "$root_dir/server/security/test_fleet_manifests.py"
python3 "$root_dir/server/security/test_compose_manifests.py"
python3 "$root_dir/server/security/test_kubernetes_policies.py"
python3 "$root_dir/server/security/test_supply_chain.py"
python3 "$root_dir/scripts/verify_observability_manifests.py"
+32
View File
@@ -0,0 +1,32 @@
from pathlib import Path
import unittest
ROOT = Path(__file__).parents[2]
class ComposeManifestTest(unittest.TestCase):
def test_allocated_fixture_is_independent_of_legacy_smoke(self):
allocated = (ROOT / "compose.allocated-smoke.yml").read_text()
legacy = (ROOT / "compose.phase6-smoke.yml").read_text()
self.assertIn("target: testkit-api", allocated)
self.assertIn('"18080:8080"', allocated)
self.assertNotIn("compose.phase6-smoke.yml", allocated)
self.assertNotIn("18080:8080", legacy)
self.assertNotIn("max-matches", allocated)
def test_allocated_runner_checks_durable_retry_and_shutdown(self):
runner = (ROOT / "scripts/verify_allocated_compose.sh").read_text()
for marker in (
"/v1/servers/compose-server/result",
"compose-result-key-123456",
"result_receipts",
"/v1/servers/compose-server/shutdown",
"SERVER_SHUTDOWN",
"down --volumes --remove-orphans",
):
self.assertIn(marker, runner)
if __name__ == "__main__":
unittest.main()