From 0505cd7ff17711a95e1e1abb17309e981af10ed2 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Thu, 20 Aug 2026 14:03:53 +0000 Subject: [PATCH 1/3] WIP checkpoint: Fix: navigating Play -> Back logs the user out (should stay logged in) --- scripts/main_menu.gd | 54 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index 6cea46e..a38ae4e 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -8,9 +8,14 @@ extends Control ## ## On successful auth the token is stored via ApiClient, AuthButtonsRow hides, ## the welcome label shows the username, and the Play button becomes enabled. +## +## When returning from another scene (e.g. world_list), _ready() checks whether +## ApiClient already holds a valid token. If so, it restores the authenticated +## UI state (Play enabled, welcome label visible) instead of forcing the user +## back to the login form. -# ── UI references ──────────────────────────────────────────────────────────── +# ─── UI references ──────────────────────────────────────────────────────────── var _auth_buttons_row: HBoxContainer var _login_button: Button @@ -32,7 +37,7 @@ var _is_register_mode: bool = false var _is_authenticating: bool = false -# ── Lifecycle ──────────────────────────────────────────────────────────────── +# ─── Lifecycle ──────────────────────────────────────────────────────────────── func _ready() -> void: # Cache node references. @@ -52,7 +57,38 @@ func _ready() -> void: _play_button = $CenterContainer/VBoxContainer/PlayButton _quit_button = $CenterContainer/VBoxContainer/QuitButton - # Initial state. + # Check whether a valid session already exists (e.g. user logged in, + # navigated to world_list, then clicked Back). If so, restore the + # authenticated UI state instead of forcing the login form again. + if ApiClient.has_token(): + var me_result: Dictionary = await ApiClient.get_me() + if me_result.ok and me_result.data is Dictionary: + var username: String = str((me_result.data as Dictionary).get("username", "")) + if username != "": + _show_authenticated(username) + return + + # Token missing or invalid — fall through to logged-out state. + _reset_to_logged_out() + + +## Restore the UI to the authenticated state (Play enabled, welcome label, +## auth buttons hidden). +func _show_authenticated(username: String) -> void: + _auth_panel.visible = false + _status_label.visible = false + _welcome_label.text = "Welcome, %s!" % username + _welcome_label.visible = true + _email_edit.visible = false + _dob_edit.visible = false + _play_button.disabled = false + _auth_buttons_row.visible = false + print("Restored authenticated session for: ", username) + + +## Reset the UI to the logged-out state (Play disabled, auth panel hidden, +## login/register buttons visible). +func _reset_to_logged_out() -> void: _auth_panel.visible = false _status_label.visible = false _welcome_label.visible = false @@ -62,7 +98,7 @@ func _ready() -> void: _update_auth_mode_ui() -# ── Play / Quit ────────────────────────────────────────────────────────────── +# ─── Play / Quit ───────────────────────────────────────────────────────────── func _on_play_button_pressed() -> void: if _play_button.disabled: @@ -76,7 +112,7 @@ func _on_quit_button_pressed() -> void: get_tree().quit() -# ── Auth panel toggle ──────────────────────────────────────────────────────── +# ─── Auth panel toggle ─────────────────────────────────────────────────────── func _on_login_button_pressed() -> void: """Show the auth panel, called from AuthButtonsRow/LoginButton.""" @@ -94,7 +130,7 @@ func _on_register_button_pressed() -> void: _username_edit.grab_focus() -# ── Mode toggle (Login / Register) ─────────────────────────────────────────── +# ─── Mode toggle (Login / Register) ────────────────────────────────────────── func _on_mode_toggle_toggled(button_pressed: bool) -> void: _is_register_mode = button_pressed @@ -116,7 +152,7 @@ func _update_auth_mode_ui() -> void: _dob_edit.visible = false -# ── Auth submission ────────────────────────────────────────────────────────── +# ─── Auth submission ───────────────────────────────────────────────────────── func _on_auth_button_pressed() -> void: """Handle login or register button press.""" @@ -187,7 +223,7 @@ func _on_auth_complete(result: Dictionary) -> void: _show_status(result.detail) -# ── Status helpers ─────────────────────────────────────────────────────────── +# ─── Status helpers ────────────────────────────────────────────────────────── func _show_status(message: String) -> void: _status_label.text = message @@ -199,7 +235,7 @@ func _clear_status() -> void: _status_label.visible = false -# ── Validation ─────────────────────────────────────────────────────────────── +# ─── Validation ────────────────────────────────────────────────────────────── func _is_valid_date(date_str: String) -> bool: """Check if a string looks like a valid YYYY-MM-DD date.""" -- 2.52.0 From c01f9de586a8fae4d45e18f2ec0fc366d3255a6d Mon Sep 17 00:00:00 2001 From: coder-bot Date: Thu, 20 Aug 2026 15:04:19 +0000 Subject: [PATCH 2/3] WIP checkpoint: Fix: navigating Play -> Back logs the user out (should stay logged in) --- scripts/smoke_test_main_menu_back.gd | 147 +++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 scripts/smoke_test_main_menu_back.gd diff --git a/scripts/smoke_test_main_menu_back.gd b/scripts/smoke_test_main_menu_back.gd new file mode 100644 index 0000000..c8c6758 --- /dev/null +++ b/scripts/smoke_test_main_menu_back.gd @@ -0,0 +1,147 @@ +extends SceneTree +## Regression test for the Play -> Back navigation bug. +## +## The reported bug: after logging in and clicking Play, the user lands on +## world_list.tscn; clicking Back returns to main_menu.tscn, but the menu +## reset to a fully logged-out state (Play disabled, login form showing) +## even though ApiClient still held a valid token. main_menu.gd's _ready() +## used to unconditionally reset the UI on every scene load. +## +## This test drives the real navigation flow (change_scene_to_file) against +## a live backend: +## 1. Register a unique user so ApiClient holds a valid token. +## 2. Load main_menu.tscn and assert it restores the authenticated state. +## 3. Click Play -> world_list.tscn. +## 4. Click Back -> main_menu.tscn. +## 5. Assert the menu is STILL authenticated (Play enabled, welcome label +## visible, login/register buttons hidden). +## +## Run with: +## godot --headless --path . --script res://scripts/smoke_test_main_menu_back.gd +## +## Requires the backend to be running (default base URL +## `application/agp/api_base_url` -> http://127.0.0.1:8000). + +const USERNAME := "backnav_%d" +const EMAIL := "backnav_%d@test.example.com" +const PASSWORD := "BackNav123!" +const DOB := "2000-01-01" + +var passed := 0 +var failed := 0 + + +func _initialize() -> void: + _run() + + +func _run() -> void: + await process_frame + + var api := root.get_node("ApiClient") + if api == null: + printerr("FAIL: ApiClient autoload not found") + quit(1) + return + + # 1. Establish a real, valid session exactly the way a user would. + var unique_id := Time.get_ticks_msec() + var username := USERNAME % unique_id + var email := EMAIL % unique_id + print("\n=== Setup: register ", username, " ===") + var register_result: Dictionary = await api.register(username, email, PASSWORD, DOB) + if not _check("register succeeds", register_result.ok): + print(" detail: ", register_result.detail) + _finish() + return + _check("ApiClient stores token after register", api.has_token()) + + # 2. First visit to the main menu: should already show the authenticated + # state (no login form, Play enabled). + change_scene_to_file("res://scenes/main_menu.tscn") + await _wait_for_menu_ready() + + var menu: Control = current_scene + if menu == null or not menu is Control: + printerr("FAIL: main_menu.tscn did not become current_scene") + _finish() + return + _check("first main menu visit restores authenticated state", _is_authenticated(menu)) + + # 3. Play -> world_list.tscn (real button press). + var play_button: Button = menu.get_node("CenterContainer/VBoxContainer/PlayButton") + play_button.pressed.emit() + await _wait_for_scene_change(menu) + + var world_list_scene := current_scene + if world_list_scene == null or world_list_scene == menu: + printerr("FAIL: world_list.tscn did not become current_scene after Play") + _finish() + return + + # 4. Back -> main_menu.tscn (real button press). + var back_button: Button = world_list_scene.get_node("VBoxContainer/HeaderHBox/BackButton") + back_button.pressed.emit() + await _wait_for_scene_change(world_list_scene) + + menu = current_scene + if menu == null or not menu is Control: + printerr("FAIL: main_menu.tscn did not become current_scene after Back") + _finish() + return + + # 5. THE regression assertion: still logged in after returning. + _check("main menu stays authenticated after Play -> Back", _is_authenticated(menu)) + + _finish() + + +func _is_authenticated(menu: Control) -> bool: + var play_button: Button = menu.get_node("CenterContainer/VBoxContainer/PlayButton") + var welcome_label: Label = menu.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel") + var auth_buttons_row: HBoxContainer = menu.get_node("CenterContainer/VBoxContainer/AuthButtonsRow") + var auth_panel: PanelContainer = menu.get_node("CenterContainer/VBoxContainer/AuthPanel") + + _check("Play enabled", not play_button.disabled) + _check("Welcome label visible", welcome_label.visible) + _check("Auth buttons row hidden", not auth_buttons_row.visible) + _check("Auth panel hidden", not auth_panel.visible) + + return (not play_button.disabled) \ + and welcome_label.visible \ + and not auth_buttons_row.visible \ + and not auth_panel.visible + + +## Wait for the main menu's _ready() to finish, including its awaited +## ApiClient.get_me() call (which is why we wait several frames). +func _wait_for_menu_ready() -> void: + for i in 20: + await process_frame + if current_scene is Control: + # The menu's _ready() awaits get_me(); give it time to resolve. + if i >= 1: + return + await process_frame + + +func _wait_for_scene_change(previous_scene: Node) -> void: + for i in 20: + await process_frame + if current_scene != previous_scene and current_scene != null: + return + + +func _check(description: String, condition: bool) -> bool: + if condition: + print(" PASS: ", description) + passed += 1 + else: + printerr(" FAIL: ", description) + failed += 1 + return condition + + +func _finish() -> void: + print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed]) + quit(0 if failed == 0 else 1) -- 2.52.0 From c287202b9d8b7ee1a49ab7381f0ac5fb204376e1 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Thu, 20 Aug 2026 16:12:16 +0000 Subject: [PATCH 3/3] Fix flaky regression test for Play->Back session persistence The actual fix (main_menu.gd's _ready() checking ApiClient.has_token() and restoring authenticated state via get_me()) was already correct -- verified this directly by instrumenting frame-by-frame state and seeing it resolve properly in ~5 frames. The test wrapping it was the real problem: _wait_for_menu_ready() only waited 2 frames total before checking assertions, nowhere near enough for a real HTTP round-trip, so every run failed on a fix that actually worked. Root fix: added a real auth_state_ready signal to main_menu.gd, emitted once _ready()'s session check has actually resolved (either branch -- authenticated or logged-out). The test now awaits that signal directly instead of guessing from a frame count or from UI properties that are ambiguous mid-await (AuthButtonsRow.visible is true both before the check resolves AND in a genuine logged-out end state, which is exactly what made an earlier polling-based fix attempt here also flaky). Also fixed a second real bug in the test itself: unique_id used Time.get_ticks_msec() (process-relative, not wall-clock), colliding with a previous run's still-present data -- same flakiness class fixed elsewhere this session, same fix (wall-clock ms + random suffix). Verified: 12/12 passing across 3 consecutive runs against a real backend. Co-Authored-By: Claude Sonnet 5 --- scripts/main_menu.gd | 10 +++++++++ scripts/smoke_test_main_menu_back.gd | 31 +++++++++++++++------------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index a38ae4e..605feee 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -15,6 +15,14 @@ extends Control ## back to the login form. +## Emitted once _ready()'s session check (has_token() + the awaited +## get_me() call) has actually resolved, whichever way -- lets a test (or +## anything else) know the UI state is trustworthy now, rather than +## guessing from a frame count or from UI properties that are ambiguous +## mid-await (e.g. AuthButtonsRow.visible is true both before the check +## resolves AND in a genuine logged-out end state). +signal auth_state_ready + # ─── UI references ──────────────────────────────────────────────────────────── var _auth_buttons_row: HBoxContainer @@ -66,10 +74,12 @@ func _ready() -> void: var username: String = str((me_result.data as Dictionary).get("username", "")) if username != "": _show_authenticated(username) + auth_state_ready.emit() return # Token missing or invalid — fall through to logged-out state. _reset_to_logged_out() + auth_state_ready.emit() ## Restore the UI to the authenticated state (Play enabled, welcome label, diff --git a/scripts/smoke_test_main_menu_back.gd b/scripts/smoke_test_main_menu_back.gd index c8c6758..621a31e 100644 --- a/scripts/smoke_test_main_menu_back.gd +++ b/scripts/smoke_test_main_menu_back.gd @@ -45,7 +45,8 @@ func _run() -> void: return # 1. Establish a real, valid session exactly the way a user would. - var unique_id := Time.get_ticks_msec() + randomize() + var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000 var username := USERNAME % unique_id var email := EMAIL % unique_id print("\n=== Setup: register ", username, " ===") @@ -59,13 +60,21 @@ func _run() -> void: # 2. First visit to the main menu: should already show the authenticated # state (no login form, Play enabled). change_scene_to_file("res://scenes/main_menu.tscn") - await _wait_for_menu_ready() + # change_scene_to_file defers the actual swap -- a couple frames is + # plenty for the scene object itself to exist (unlike the real + # get_me() network round-trip afterward, which is what auth_state_ready + # actually waits for below). + for i in 5: + await process_frame + if current_scene is Control: + break var menu: Control = current_scene if menu == null or not menu is Control: printerr("FAIL: main_menu.tscn did not become current_scene") _finish() return + await menu.auth_state_ready _check("first main menu visit restores authenticated state", _is_authenticated(menu)) # 3. Play -> world_list.tscn (real button press). @@ -90,6 +99,12 @@ func _run() -> void: _finish() return + # The new main_menu instance's own _ready() is *also* awaiting a real + # get_me() call -- _wait_for_scene_change only confirms the scene + # object exists, not that its authenticated-state fields have been + # set yet. Wait for the real signal, not a guess. + await menu.auth_state_ready + # 5. THE regression assertion: still logged in after returning. _check("main menu stays authenticated after Play -> Back", _is_authenticated(menu)) @@ -113,18 +128,6 @@ func _is_authenticated(menu: Control) -> bool: and not auth_panel.visible -## Wait for the main menu's _ready() to finish, including its awaited -## ApiClient.get_me() call (which is why we wait several frames). -func _wait_for_menu_ready() -> void: - for i in 20: - await process_frame - if current_scene is Control: - # The menu's _ready() awaits get_me(); give it time to resolve. - if i >= 1: - return - await process_frame - - func _wait_for_scene_change(previous_scene: Node) -> void: for i in 20: await process_frame -- 2.52.0