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>
This commit is contained in:
2026-08-20 16:12:16 +00:00
parent c01f9de586
commit c287202b9d
2 changed files with 27 additions and 14 deletions
+10
View File
@@ -15,6 +15,14 @@ extends Control
## back to the login form. ## 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 ──────────────────────────────────────────────────────────── # ─── UI references ────────────────────────────────────────────────────────────
var _auth_buttons_row: HBoxContainer var _auth_buttons_row: HBoxContainer
@@ -66,10 +74,12 @@ func _ready() -> void:
var username: String = str((me_result.data as Dictionary).get("username", "")) var username: String = str((me_result.data as Dictionary).get("username", ""))
if username != "": if username != "":
_show_authenticated(username) _show_authenticated(username)
auth_state_ready.emit()
return return
# Token missing or invalid — fall through to logged-out state. # Token missing or invalid — fall through to logged-out state.
_reset_to_logged_out() _reset_to_logged_out()
auth_state_ready.emit()
## Restore the UI to the authenticated state (Play enabled, welcome label, ## Restore the UI to the authenticated state (Play enabled, welcome label,
+17 -14
View File
@@ -45,7 +45,8 @@ func _run() -> void:
return return
# 1. Establish a real, valid session exactly the way a user would. # 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 username := USERNAME % unique_id
var email := EMAIL % unique_id var email := EMAIL % unique_id
print("\n=== Setup: register ", username, " ===") print("\n=== Setup: register ", username, " ===")
@@ -59,13 +60,21 @@ func _run() -> void:
# 2. First visit to the main menu: should already show the authenticated # 2. First visit to the main menu: should already show the authenticated
# state (no login form, Play enabled). # state (no login form, Play enabled).
change_scene_to_file("res://scenes/main_menu.tscn") 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 var menu: Control = current_scene
if menu == null or not menu is Control: if menu == null or not menu is Control:
printerr("FAIL: main_menu.tscn did not become current_scene") printerr("FAIL: main_menu.tscn did not become current_scene")
_finish() _finish()
return return
await menu.auth_state_ready
_check("first main menu visit restores authenticated state", _is_authenticated(menu)) _check("first main menu visit restores authenticated state", _is_authenticated(menu))
# 3. Play -> world_list.tscn (real button press). # 3. Play -> world_list.tscn (real button press).
@@ -90,6 +99,12 @@ func _run() -> void:
_finish() _finish()
return 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. # 5. THE regression assertion: still logged in after returning.
_check("main menu stays authenticated after Play -> Back", _is_authenticated(menu)) _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 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: func _wait_for_scene_change(previous_scene: Node) -> void:
for i in 20: for i in 20:
await process_frame await process_frame