mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
test(multiplayer): add allocated compose smoke flow
This commit is contained in:
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.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 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/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.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.
|
||||
|
||||
|
||||
Executable
+89
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user