Files
CosmicClash/Game/tests/control_plane_smoke.gd
T
Josh Creek f7657ad9ad test(multiplayer): extend the real integration test to cover queue cancel
Adds a fourth real round trip to control_plane_smoke.gd: heartbeat ->
cancel_queue -> CANCELLED, using the same idle-wait pattern the
heartbeat step already needed. Verified stable across 3 consecutive
full runs (real Postgres, real testkit-api, real headless Godot
client), plus the full Go and Godot unit suites clean.
2026-09-01 14:16:06 +01:00

139 lines
5.6 KiB
GDScript

extends Node
# Real end-to-end smoke test for ControlPlaneClient against a REAL running
# control-plane HTTP server backed by REAL PostgreSQL -- proving the actual
# wire format (GDScript's HTTPRequest/JSON on one side, the real compiled Go
# api.Service on the other) is compatible, not just that each side's own unit
# tests pass in isolation. Every other ControlPlaneClient test in this repo
# is either pure parsing/validation logic or drives the client against a
# mock; nothing before this exercised a real network round trip end to end
# (multiplayer-next.md 8.40's own evidence names this "live... verification"
# as remaining).
#
# Run against scripts/verify_control_plane_integration.sh's server/cmd/testkit-api
# instance -- see that script's own header for why a separate, clearly-marked
# test-only binary exists rather than a flag on the real cmd/control-plane:
#
# godot --headless --path Game res://tests/control_plane_smoke.tscn -- \
# --control-plane-url=http://127.0.0.1:PORT
#
# Prints one "SMOKE PASS/FAIL: ..." line and exits 0/1.
const TIMEOUT_SECONDS := 10.0
var _finished := false
var _ticket_id := ""
func _ready() -> void:
var control_plane_url := ""
for arg in OS.get_cmdline_user_args():
if arg.begins_with("--control-plane-url="):
control_plane_url = arg.substr("--control-plane-url=".length())
if control_plane_url.is_empty():
_finish(false, "missing --control-plane-url")
return
# A syntactically valid but semantically meaningless placeholder token:
# configure() validates format eagerly, but real auth doesn't exist until
# login_steam()'s response overwrites it below. There is no other way to
# set base_url alone.
if not ControlPlaneClient.configure(control_plane_url, "0:0"):
_finish(false, "configure() rejected a valid-looking base URL")
return
_ticket_id = "smoke-ticket-%d" % Time.get_unix_time_from_system()
ControlPlaneClient.request_succeeded.connect(_on_request_succeeded)
ControlPlaneClient.request_failed.connect(_on_request_failed)
var web_api_ticket := "smoke-web-api-ticket-%d" % Time.get_ticks_usec()
var err := ControlPlaneClient.login_steam(web_api_ticket)
if err != OK:
_finish(false, "login_steam() failed to start: %s" % error_string(err))
return
print("SMOKE: logging in against %s..." % control_plane_url)
var timer := Timer.new()
timer.wait_time = TIMEOUT_SECONDS
timer.one_shot = true
timer.timeout.connect(func(): _finish(false, "timed out after %.1fs" % TIMEOUT_SECONDS))
add_child(timer)
timer.start()
func _on_request_succeeded(operation: String, payload: Dictionary) -> void:
if _finished:
return
match operation:
"steam_session":
print("SMOKE: logged in as %s, creating a queue ticket..." % ControlPlaneClient.player_id)
var err := ControlPlaneClient.queue_create(_ticket_id, "casual", "smoke-build", 1)
if err != OK:
_finish(false, "queue_create() failed to start: %s" % error_string(err))
"queue_create":
if payload.get("ticket_id", "") != _ticket_id or payload.get("state", "") != "QUEUED":
_finish(false, "unexpected queue_create payload: %s" % payload)
return
# apply_ticket_update (called for every "queue_"-prefixed response,
# including this one) can itself decide the ticket needs a resync
# and fire off a recover_queue() call -- a real, existing part of
# MatchmakingState's own state machine, not something this test
# controls. Wait for ControlPlaneClient to go idle before sending
# the next request rather than assuming queue_create was the only
# thing in flight.
print("SMOKE: ticket %s QUEUED at revision %d, heartbeating once idle..." % [_ticket_id, ControlPlaneClient.state.revision])
_send_heartbeat_once_idle()
"queue_recover":
pass # Expected background resync; the idle-wait above handles it.
"queue_heartbeat":
if int(payload.get("revision", -1)) <= 0:
_finish(false, "heartbeat did not advance the revision: %s" % payload)
return
print("SMOKE: heartbeat advanced to revision %d, cancelling..." % ControlPlaneClient.state.revision)
_send_cancel_once_idle()
"queue_cancel":
if payload.get("state", "") != "CANCELLED":
_finish(false, "unexpected queue_cancel payload: %s" % payload)
return
_finish(true, "login -> queue_create -> heartbeat -> cancel all round-tripped against a real server")
func _send_heartbeat_once_idle() -> void:
if not ControlPlaneClient._operation.is_empty():
# call_deferred alone floods the message queue without ever letting a
# real frame (and therefore the in-flight HTTP request) actually
# process -- a real Timer yields to the engine between checks.
var poll := get_tree().create_timer(0.05)
poll.timeout.connect(_send_heartbeat_once_idle)
return
var err := ControlPlaneClient.heartbeat(_ticket_id, ControlPlaneClient.state.revision)
if err != OK:
_finish(false, "heartbeat() failed to start: %s" % error_string(err))
func _send_cancel_once_idle() -> void:
if not ControlPlaneClient._operation.is_empty():
var poll := get_tree().create_timer(0.05)
poll.timeout.connect(_send_cancel_once_idle)
return
var err := ControlPlaneClient.cancel_queue(_ticket_id, ControlPlaneClient.state.revision)
if err != OK:
_finish(false, "cancel_queue() failed to start: %s" % error_string(err))
func _on_request_failed(operation: String, http_code: int, detail: String) -> void:
if _finished:
return
_finish(false, "%s failed: http=%d detail=%s" % [operation, http_code, detail])
func _finish(passed: bool, detail: String) -> void:
if _finished:
return
_finished = true
if passed:
print("SMOKE PASS: %s" % detail)
get_tree().quit(0)
else:
print("SMOKE FAIL: %s" % detail)
get_tree().quit(1)