fix(client): defer companion close-interceptor re-open out of the app_changed emit (PR #183 review)
Tyre's finding: _on_hud_app_changed ran open_app synchronously from INSIDE close_app's app_changed emit; close_app continues past the emit and resets _active_app, clobbering the re-open — Atlas left visible (z already raised) but is_app_active() false, so atlas_app's input guard rejects every key: keyboard soft-lock after one M/Escape on the reach screen. Fix: open_app.call_deferred, out of the signal frame. Two regression tests pin the contract at the HudGroups level (no server needed): the synchronous shape must keep getting clobbered and the deferred shape must survive — if HudGroups emit semantics ever change, both flag the contract shift for a deliberate look. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,13 @@ func _exit_tree() -> void:
|
||||
## else to show). atlas_app.gd is never modified.
|
||||
func _on_hud_app_changed(app_path: String, mode: int) -> void:
|
||||
if app_path == FRONT_APP and mode == HudGroups.Mode.GAMEPLAY:
|
||||
HudGroups.open_app(FRONT_APP)
|
||||
# Deferred, never synchronous: this handler runs INSIDE close_app()'s
|
||||
# app_changed emit, and close_app() continues after the emit and resets
|
||||
# _active_app to "" — a synchronous open_app() here gets clobbered,
|
||||
# leaving the Atlas visible (open_app already raised its z) but
|
||||
# is_app_active() false, which kills atlas_app's input guard: a
|
||||
# keyboard soft-lock (PR #183 review, Tyre).
|
||||
HudGroups.open_app.call_deferred(FRONT_APP)
|
||||
|
||||
|
||||
func _boot() -> void:
|
||||
|
||||
@@ -12,11 +12,14 @@ extends GdUnitTestSuite
|
||||
## Deliberately NOT covered here (would require a live server / real window,
|
||||
## per the ticket's "VERIFY LIVE once" instruction rather than an automated
|
||||
## suite): the full _boot() sequence, the attach-then-fallback-to-spawn
|
||||
## control flow, the HudGroups close-interception behavior, and the
|
||||
## orphan-on-exit server lifecycle. Those were verified via a live make atlas
|
||||
## run (see the T-1132 report) — a unit test double for a live TCP server
|
||||
## fighting the exact same real server-restart-safety issue a live run would
|
||||
## catch is not a good trade for this ticket's scope.
|
||||
## control flow, and the orphan-on-exit server lifecycle. Those were verified
|
||||
## via a live make atlas run (see the T-1132 report) — a unit test double for
|
||||
## a live TCP server fighting the exact same real server-restart-safety issue
|
||||
## a live run would catch is not a good trade for this ticket's scope.
|
||||
## The close-interception CONTRACT (deferred re-open, PR #183 review) IS
|
||||
## pinned below at the HudGroups level — it needs no server, and the sync
|
||||
## variant's soft-lock is exactly the kind of bug a live smoke plausibly
|
||||
## misses.
|
||||
##
|
||||
## Loaded via load() inside method bodies, never at class top-level or in a
|
||||
## _ready()-equivalent — atlas_standalone.gd's own _ready() references the
|
||||
@@ -29,6 +32,9 @@ extends GdUnitTestSuite
|
||||
## not just for autoload scripts themselves.
|
||||
const SCRIPT_PATH := "res://scripts/atlas_standalone.gd"
|
||||
|
||||
# The companion's front app (see the close-interception contract tests below).
|
||||
const FRONT := "implant/map"
|
||||
|
||||
|
||||
func _script():
|
||||
return load(SCRIPT_PATH)
|
||||
@@ -136,3 +142,50 @@ func test_parse_listening_line_unrelated_log_line_returns_negative_one() -> void
|
||||
assert_that(
|
||||
s._parse_listening_line("Waiting for client connection on port 9876")
|
||||
).is_equal(-1)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Close-interception contract (PR #183 review, Tyre) — the companion's re-open
|
||||
# of the front app MUST be deferred out of the app_changed emit frame.
|
||||
# close_app() continues past its emit and resets _active_app to ""; a
|
||||
# synchronous open_app() from the handler is silently clobbered, leaving the
|
||||
# app visible (its z was already raised) but is_app_active() false — which
|
||||
# kills the app's input guard: a keyboard soft-lock. Pinned here at the
|
||||
# HudGroups level (no server, no window) so the deferred call in
|
||||
# atlas_standalone._on_hud_app_changed can't be "simplified" back to sync.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func _sync_reopen(app_path: String, mode: int) -> void:
|
||||
if app_path == FRONT and mode == HudGroups.Mode.GAMEPLAY:
|
||||
HudGroups.open_app(FRONT)
|
||||
|
||||
|
||||
func _deferred_reopen(app_path: String, mode: int) -> void:
|
||||
if app_path == FRONT and mode == HudGroups.Mode.GAMEPLAY:
|
||||
HudGroups.open_app.call_deferred(FRONT)
|
||||
|
||||
|
||||
func test_synchronous_reopen_from_close_emit_is_clobbered() -> void:
|
||||
# Documents the bug class: this is the OLD interceptor shape, and it must
|
||||
# keep failing to keep the app active — if HudGroups semantics ever change
|
||||
# so sync re-open works, both this test and the deferred one below flag
|
||||
# the contract shift for a deliberate look.
|
||||
HudGroups.open_app(FRONT)
|
||||
HudGroups.app_changed.connect(_sync_reopen)
|
||||
HudGroups.close_app()
|
||||
HudGroups.app_changed.disconnect(_sync_reopen)
|
||||
assert_bool(HudGroups.is_app_active(FRONT)).is_false()
|
||||
HudGroups.close_app() # restore clean autoload state for the next test
|
||||
|
||||
|
||||
func test_deferred_reopen_survives_close_emit() -> void:
|
||||
# The shipped interceptor shape (atlas_standalone.gd:_on_hud_app_changed).
|
||||
HudGroups.open_app(FRONT)
|
||||
HudGroups.app_changed.connect(_deferred_reopen)
|
||||
HudGroups.close_app()
|
||||
HudGroups.app_changed.disconnect(_deferred_reopen)
|
||||
assert_bool(HudGroups.is_app_active(FRONT)).is_false() # not yet — deferred
|
||||
await get_tree().process_frame
|
||||
assert_bool(HudGroups.is_app_active(FRONT)).is_true()
|
||||
HudGroups.close_app() # restore clean autoload state
|
||||
|
||||
Reference in New Issue
Block a user