extends Node # Two-process real end-to-end proposal smoke test: extends # control_plane_smoke.gd's single-player login/queue/heartbeat/cancel # coverage to the matcher path -- two real Godot clients, two real queued # tickets, a real running server/cmd/matcher pairing them, both clients # observing the resulting proposal over the real WebSocket event stream and # accepting it for real. multiplayer-next.md 8.40 names this "a two-player # proposal round trip" as the next scoped extension to this harness. # # ControlPlaneClient is a singleton autoload, so one process can only ever be # one player -- this mirrors net_smoke.gd's own host/client two-process # pattern rather than trying to simulate two players in one process: # # godot --headless --path Game res://tests/control_plane_proposal_smoke.tscn -- \ # --control-plane-url=http://127.0.0.1:PORT --role=player-a # godot --headless --path Game res://tests/control_plane_proposal_smoke.tscn -- \ # --control-plane-url=http://127.0.0.1:PORT --role=player-b # # Prints one "SMOKE PASS/FAIL: ..." line and exits 0/1. const TIMEOUT_SECONDS := 20.0 var _role := "" var _finished := false var _ticket_id := "" var _accept_sent := 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("--role="): _role = arg.substr("--role=".length()) if control_plane_url.is_empty() or (_role != "player-a" and _role != "player-b"): _finish(false, "missing --control-plane-url or --role=player-a|player-b") return if not ControlPlaneClient.configure(control_plane_url, "0:0"): _finish(false, "configure() rejected a valid-looking base URL") return _ticket_id = "proposal-smoke-%s-%d" % [_role, Time.get_unix_time_from_system()] ControlPlaneClient.request_succeeded.connect(_on_request_succeeded) ControlPlaneClient.request_failed.connect(_on_request_failed) ControlPlaneClient.state.changed.connect(_on_state_changed) var web_api_ticket := "proposal-smoke-web-api-ticket-%s-%d" % [_role, 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[%s]: logging in against %s..." % [_role, 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 waiting for a proposal" % TIMEOUT_SECONDS)) add_child(timer) timer.start() func _on_request_succeeded(operation: String, payload: Dictionary) -> void: if _finished: return if operation == "steam_session": print("SMOKE[%s]: logged in as %s, queueing for a 2-player casual match..." % [_role, 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)) return if operation == "queue_create": print("SMOKE[%s]: ticket %s QUEUED, waiting for the matcher to propose a match..." % [_role, _ticket_id]) return if operation.begins_with("proposal_"): if payload.get("state", "") != "ACCEPTED" and payload.get("state", "") != "OPEN": _finish(false, "unexpected proposal response after accept: %s" % payload) return if payload.get("state", "") == "ACCEPTED": _finish(true, "both players queued, the real matcher formed a proposal, and this client's accept was recorded") else: print("SMOKE[%s]: accepted; waiting for the other player..." % _role) var recovery_timer := get_tree().create_timer(1.0) recovery_timer.timeout.connect(_recover_after_accept) func _recover_after_accept() -> void: if _finished or ControlPlaneClient._operation != "" or ControlPlaneClient.state.proposal_id.is_empty(): return var err := ControlPlaneClient.recover_proposal(ControlPlaneClient.state.proposal_id) if err != OK and err != ERR_BUSY: _finish(false, "proposal recovery after accept failed to start: %s" % error_string(err)) func _on_state_changed(snapshot: Dictionary) -> void: if _finished or _accept_sent or String(snapshot.get("proposal_state", "")) != "OPEN": return if ControlPlaneClient._operation != "": return # Already mid-request (e.g. the accept itself); avoid double-sending. print("SMOKE[%s]: proposal %s is OPEN at revision %d, accepting..." % [_role, ControlPlaneClient.state.proposal_id, ControlPlaneClient.state.proposal_revision]) _accept_sent = true if _role == "player-b": var delay := get_tree().create_timer(0.5) delay.timeout.connect(_send_accept) return _send_accept() func _send_accept() -> void: if _finished: return var err := ControlPlaneClient.respond_to_proposal(ControlPlaneClient.state.proposal_id, true, ControlPlaneClient.state.proposal_revision) if err != OK and err != ERR_BUSY: _accept_sent = false _finish(false, "respond_to_proposal() failed to start: %s" % error_string(err)) func _on_request_failed(operation: String, http_code: int, detail: String) -> void: if _finished: return if operation == "proposal_accept" and http_code == HTTPClient.RESPONSE_CONFLICT: print("SMOKE[%s]: concurrent accept conflicted; ControlPlaneClient is recovering the authoritative proposal..." % _role) 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] %s" % [_role, detail]) get_tree().quit(0) else: print("SMOKE FAIL: [%s] %s" % [_role, detail]) get_tree().quit(1)