Files
CosmicClash/Game/tests/main_menu_test_hooks.gd
T
Josh Creek 4533da34e0 feat(multiplayer): Phase 1 transport, connection, and lobby
Lands tasks 1.0-1.8 of multiplayer-todo.md: the pure-function test runner,
net_codec (wire format quantizers/pack-unpack), NetworkManager (ENet
transport, manual polling, min-RTT clock sync), MatchNet (handshake,
protocol/tick-rate gating, roster with team+ready state), lobby.tscn (team
columns, switch team, ready toggle), server_boot.tscn (headless dedicated
server with structured logging and an overrun watchdog), and main_menu.gd's
Host/Join-by-IP UI (connecting overlay, cancel, bounded failure path).

Followed by an adversarial review (Opus subagent) that found and fixed two
real bugs - an unvalidated player_name broadcast that let one client's
oversized name head-of-line-block the reliable channel for everyone, and a
server-side roster leak across a host/re-host cycle - plus three gaps in
the test suite itself where a claim of "verified" wasn't actually backed
by what the test checked. All five two-process smoke tests plus the
pure-function suite are green with the strengthened assertions in place.
2026-08-20 08:18:59 +01:00

115 lines
5.0 KiB
GDScript

extends Node
# Test-only helper (tests/main_menu_smoke.gd). Not referenced by production
# code. Registered as a temporary project autoload only while running this
# test — see the test's own header for why an autoload (rather than a
# scene-child driver) is needed: main_menu.tscn IS the real current_scene
# here (run directly via --path Game res://scenes/main_menu.tscn, exactly
# like production), so unlike lobby_smoke.gd's driver this doesn't even
# need to survive a scene swap — it just needs to exist independently of
# main_menu.gd so main_menu.gd itself stays untouched by test concerns.
var _role := ""
func _ready() -> void:
for arg in OS.get_cmdline_user_args():
if arg.begins_with("--role="):
_role = arg.substr("--role=".length())
if _role.is_empty():
return
# main_menu.gd's own _ready() (which builds %-unique-name refs) must run
# before we touch its nodes; autoloads run first, so wait a frame.
await get_tree().process_frame
await get_tree().process_frame
match _role:
"host":
_run_host()
"join_ok":
_run_join_ok()
"join_refused":
_run_join_refused()
"join_cancel":
_run_join_cancel()
func _run_host() -> void:
var menu := get_tree().current_scene
var host_btn: Button = menu.get_node("CenterContainer/VBoxContainer/HostButton")
host_btn.emit_signal("pressed")
await get_tree().create_timer(1.0).timeout
var scene := get_tree().current_scene
var ok := scene != null and scene.scene_file_path == "res://scenes/lobby.tscn"
print("SMOKE %s: host -> current_scene=%s" % ["PASS" if ok else "FAIL", scene.scene_file_path if scene else "null"])
if not ok:
_finish(false, "host transition check failed")
return
# Stay up long enough for a separate join_ok/join_refused/join_cancel
# process (started after this one) to actually exercise the host —
# unlike _finish()'s normal 0.3s beat, this test's whole point is being
# a live target for a while.
await get_tree().create_timer(6.0).timeout
_finish(true, "host ran and stayed up for a joiner")
func _run_join_ok() -> void:
# Give the host process (started first by the shell script) time to be listening.
await get_tree().create_timer(1.5).timeout
var menu := get_tree().current_scene
var address_edit: LineEdit = menu.get_node("%JoinAddressEdit")
address_edit.text = "127.0.0.1"
var join_btn: Button = menu.get_node("CenterContainer/VBoxContainer/JoinRow/JoinButton")
join_btn.emit_signal("pressed")
var overlay: Control = menu.get_node("%ConnectingOverlay")
print("SMOKE INFO: overlay visible right after Join press = %s" % str(overlay.visible))
await get_tree().create_timer(2.0).timeout
var scene := get_tree().current_scene
var ok := scene != null and scene.scene_file_path == "res://scenes/lobby.tscn"
_finish(ok, "join_ok -> current_scene=%s" % (scene.scene_file_path if scene else "null"))
func _run_join_refused() -> void:
var menu := get_tree().current_scene
var address_edit: LineEdit = menu.get_node("%JoinAddressEdit")
address_edit.text = "127.0.0.1"
var join_btn: Button = menu.get_node("CenterContainer/VBoxContainer/JoinRow/JoinButton")
join_btn.emit_signal("pressed")
var overlay: Control = menu.get_node("%ConnectingOverlay")
print("SMOKE INFO: overlay visible right after Join press (no server) = %s" % str(overlay.visible))
# main_menu.gd's own CONNECT_TIMEOUT_SECONDS (6.0) is what actually
# bounds this now — ENet's own connection_failed proved unbounded in
# practice against a genuinely refused loopback connection.
await get_tree().create_timer(8.0).timeout
var error_label: Label = menu.get_node("%MultiplayerErrorLabel")
var still_on_menu := get_tree().current_scene == menu
var ok := still_on_menu and not overlay.visible and error_label.visible
_finish(ok, "join_refused -> still_on_menu=%s overlay_visible=%s error_visible=%s error_text='%s'" % [
str(still_on_menu), str(overlay.visible), str(error_label.visible), error_label.text
])
func _run_join_cancel() -> void:
var menu := get_tree().current_scene
var address_edit: LineEdit = menu.get_node("%JoinAddressEdit")
address_edit.text = "10.255.255.1" # non-routable; connect attempt just hangs until timeout/cancel
var join_btn: Button = menu.get_node("CenterContainer/VBoxContainer/JoinRow/JoinButton")
join_btn.emit_signal("pressed")
var overlay: Control = menu.get_node("%ConnectingOverlay")
await get_tree().create_timer(0.5).timeout
var overlay_shown := overlay.visible
var cancel_btn: Button = menu.get_node("%ConnectingCancelButton")
cancel_btn.emit_signal("pressed")
await get_tree().create_timer(0.5).timeout
var still_on_menu := get_tree().current_scene == menu
var ok := overlay_shown and not overlay.visible and still_on_menu and not NetworkManager.is_client
_finish(ok, "join_cancel -> overlay_shown=%s overlay_now=%s still_on_menu=%s is_client=%s" % [
str(overlay_shown), str(overlay.visible), str(still_on_menu), str(NetworkManager.is_client)
])
func _finish(success: bool, message: String) -> void:
print("SMOKE %s: %s" % ["PASS" if success else "FAIL", message])
await get_tree().create_timer(0.3).timeout
NetworkManager.shutdown()
get_tree().quit(0 if success else 1)