mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
f7657ad9ad
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.
139 lines
5.6 KiB
GDScript
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)
|