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 := "" var _assignment_match_id := "" var _steam_ticket := "" var _ranked_profile_smoke := false 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()) elif arg.begins_with("--assignment-match-id="): _assignment_match_id = arg.substr("--assignment-match-id=".length()) elif arg.begins_with("--steam-ticket="): _steam_ticket = arg.substr("--steam-ticket=".length()) elif arg == "--ranked-profile-smoke": _ranked_profile_smoke = true 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 := _steam_ticket if not _steam_ticket.is_empty() else "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": if not _assignment_match_id.is_empty(): print("SMOKE: logged in as %s, fetching player-scoped assignment..." % ControlPlaneClient.player_id) var err := ControlPlaneClient.fetch_assignment(_assignment_match_id) if err != OK: _finish(false, "fetch_assignment() failed to start: %s" % error_string(err)) return if _ranked_profile_smoke: print("SMOKE: logged in as %s, fetching populated ranked profile..." % ControlPlaneClient.player_id) var ranked_err := ControlPlaneClient.fetch_ranked_profile() if ranked_err != OK: _finish(false, "fetch_ranked_profile() failed to start: %s" % error_string(ranked_err)) return print("SMOKE: logged in as %s, fetching ranked profile (expect none yet)..." % ControlPlaneClient.player_id) var err := ControlPlaneClient.fetch_ranked_profile() if err != OK: _finish(false, "fetch_ranked_profile() failed to start: %s" % error_string(err)) "assignment": if payload.get("match_id", "") != _assignment_match_id or payload.get("player_id", "") != ControlPlaneClient.player_id or not ControlPlaneClient.assignment.available: _finish(false, "unexpected assignment payload: %s" % payload) return _finish(true, "authenticated assignment fetch returned the player-scoped endpoint and join authorisation") "ranked_profile": if not _ranked_profile_smoke: _finish(false, "a brand-new testkit identity unexpectedly already has a ranked profile: %s" % payload) return if int(payload.get("rating", -1)) != 1600 or int(payload.get("ranked_games", -1)) != 12 or payload.get("provisional", true) != false or String(payload.get("tier", "")) != "GOLD": _finish(false, "unexpected populated ranked profile: %s" % payload) return _finish(true, "authenticated ranked profile returned the durable rating, games, tier, and provisional state") "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 if operation == "ranked_profile" and http_code == HTTPClient.RESPONSE_NOT_FOUND: # Expected: a brand-new testkit identity has no ratings row yet, and # the real store adapter reports that the same way the in-memory # fallback it replaced always did -- not found, not an error. print("SMOKE: ranked profile correctly reports not found, creating a queue ticket...") 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)) return if operation == "assignment": _finish(false, "assignment fetch failed: http=%d detail=%s" % [http_code, detail]) 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)