Compare commits

...

5 Commits

Author SHA1 Message Date
coder-bot e51c4d80b8 WIP checkpoint: Address review feedback: Fix: navigating Play -> Back logs the user out (should stay logged in) 2026-08-20 16:47:47 +00:00
architect-bot 00cdab0452 WIP checkpoint: Address review feedback: Fix: navigating Play -> Back logs the user out (should stay logged in) 2026-08-20 16:40:56 +00:00
coder-bot c287202b9d 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 <noreply@anthropic.com>
2026-08-20 16:12:16 +00:00
coder-bot c01f9de586 WIP checkpoint: Fix: navigating Play -> Back logs the user out (should stay logged in) 2026-08-20 15:04:19 +00:00
coder-bot 0505cd7ff1 WIP checkpoint: Fix: navigating Play -> Back logs the user out (should stay logged in) 2026-08-20 14:03:53 +00:00
3 changed files with 216 additions and 10 deletions
+61 -9
View File
@@ -8,9 +8,22 @@ extends Control
## ##
## On successful auth the token is stored via ApiClient, AuthButtonsRow hides, ## On successful auth the token is stored via ApiClient, AuthButtonsRow hides,
## the welcome label shows the username, and the Play button becomes enabled. ## 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 ──────────────────────────────────────────────────────────── ## 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 var _auth_buttons_row: HBoxContainer
var _login_button: Button var _login_button: Button
@@ -32,7 +45,7 @@ var _is_register_mode: bool = false
var _is_authenticating: bool = false var _is_authenticating: bool = false
# ── Lifecycle ──────────────────────────────────────────────────────────────── # ─── Lifecycle ────────────────────────────────────────────────────────────────
func _ready() -> void: func _ready() -> void:
# Cache node references. # Cache node references.
@@ -52,7 +65,46 @@ func _ready() -> void:
_play_button = $CenterContainer/VBoxContainer/PlayButton _play_button = $CenterContainer/VBoxContainer/PlayButton
_quit_button = $CenterContainer/VBoxContainer/QuitButton _quit_button = $CenterContainer/VBoxContainer/QuitButton
# Initial state. # Default to Play being gated. When a token exists, the async session
# check below may take several frames (a real get_me() round-trip);
# keeping the button disabled until validation succeeds prevents a fast
# click from navigating to world_list with an expired/invalid token.
_play_button.disabled = true
# 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)
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,
## auth buttons hidden).
func _show_authenticated(username: String) -> void:
_auth_panel.visible = true
_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 _auth_panel.visible = false
_status_label.visible = false _status_label.visible = false
_welcome_label.visible = false _welcome_label.visible = false
@@ -62,7 +114,7 @@ func _ready() -> void:
_update_auth_mode_ui() _update_auth_mode_ui()
# ── Play / Quit ────────────────────────────────────────────────────────────── # ─── Play / Quit ─────────────────────────────────────────────────────────────
func _on_play_button_pressed() -> void: func _on_play_button_pressed() -> void:
if _play_button.disabled: if _play_button.disabled:
@@ -76,7 +128,7 @@ func _on_quit_button_pressed() -> void:
get_tree().quit() get_tree().quit()
# ── Auth panel toggle ──────────────────────────────────────────────────────── # ─── Auth panel toggle ───────────────────────────────────────────────────────
func _on_login_button_pressed() -> void: func _on_login_button_pressed() -> void:
"""Show the auth panel, called from AuthButtonsRow/LoginButton.""" """Show the auth panel, called from AuthButtonsRow/LoginButton."""
@@ -94,7 +146,7 @@ func _on_register_button_pressed() -> void:
_username_edit.grab_focus() _username_edit.grab_focus()
# ── Mode toggle (Login / Register) ─────────────────────────────────────────── # ─── Mode toggle (Login / Register) ──────────────────────────────────────────
func _on_mode_toggle_toggled(button_pressed: bool) -> void: func _on_mode_toggle_toggled(button_pressed: bool) -> void:
_is_register_mode = button_pressed _is_register_mode = button_pressed
@@ -116,7 +168,7 @@ func _update_auth_mode_ui() -> void:
_dob_edit.visible = false _dob_edit.visible = false
# ── Auth submission ────────────────────────────────────────────────────────── # ─── Auth submission ─────────────────────────────────────────────────────────
func _on_auth_button_pressed() -> void: func _on_auth_button_pressed() -> void:
"""Handle login or register button press.""" """Handle login or register button press."""
@@ -187,7 +239,7 @@ func _on_auth_complete(result: Dictionary) -> void:
_show_status(result.detail) _show_status(result.detail)
# ── Status helpers ─────────────────────────────────────────────────────────── # ─── Status helpers ──────────────────────────────────────────────────────────
func _show_status(message: String) -> void: func _show_status(message: String) -> void:
_status_label.text = message _status_label.text = message
@@ -199,7 +251,7 @@ func _clear_status() -> void:
_status_label.visible = false _status_label.visible = false
# ── Validation ─────────────────────────────────────────────────────────────── # ─── Validation ──────────────────────────────────────────────────────────────
func _is_valid_date(date_str: String) -> bool: func _is_valid_date(date_str: String) -> bool:
"""Check if a string looks like a valid YYYY-MM-DD date.""" """Check if a string looks like a valid YYYY-MM-DD date."""
+154
View File
@@ -0,0 +1,154 @@
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
## actually visible in the tree, 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.
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, " ===")
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")
# 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).
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
# 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))
_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)
# WelcomeLabel lives inside AuthPanel, so `.visible` alone is a
# false-positive: the label can have visible=true while its parent is
# hidden and it renders as nothing. `is_visible_in_tree()` is the
# check that proves the user can actually see the welcome message.
_check("Welcome label visible in tree", welcome_label.is_visible_in_tree())
_check("Auth buttons row hidden", not auth_buttons_row.visible)
_check("Auth panel visible (it contains the welcome label)", auth_panel.visible)
return (not play_button.disabled) \
and welcome_label.is_visible_in_tree() \
and not auth_buttons_row.visible \
and auth_panel.visible
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)
+1 -1
View File
@@ -60,7 +60,7 @@ func _run() -> void:
await instance._on_auth_button_pressed() await instance._on_auth_button_pressed()
_check("Play became enabled after real register submission", not play_button.disabled) _check("Play became enabled after real register submission", not play_button.disabled)
_check("Welcome label visible", welcome_label.visible) _check("Welcome label visible in tree", welcome_label.is_visible_in_tree())
_check("Auth buttons row hid itself after success", not auth_buttons_row.visible) _check("Auth buttons row hid itself after success", not auth_buttons_row.visible)
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed]) print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])