test(ui): pin the _user_adjusted resize guard both ways; fix coalescing doc overstatement (PR #188 review)

Hoshe's finding: the flag introduced so auto-fit never fights a manual
view had zero coverage on exactly that branch. Two tests drive the
REAL _gui_input path (synthetic drag), then fire NOTIFICATION_RESIZED:
user-adjusted view survives a resize untouched (zoom AND offset);
an unadjusted view re-fits to the new viewport. 52/52.

Non-blocking doc note also taken: layer_proxy's normalization comment
claimed the twins would otherwise 'coalesce independently' — the
coalescing key is (ConnectionId, body_id) and never carried center;
rewritten to say what normalization actually buys on that path (the
work item derives and echoes the canonical center).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 15:22:24 +02:00
co-authored by Claude Fable 5
parent 831625019a
commit 7ce6cc08fd
2 changed files with 70 additions and 3 deletions
+63
View File
@@ -430,3 +430,66 @@ func test_center_one_column_past_the_seam_shares_a_cache_key_with_its_twin() ->
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v1.get_district_window()).is_equal(window) assert_that(v1.get_district_window()).is_equal(window)
assert_that(v2.get_district_window()).is_equal(window) assert_that(v2.get_district_window()).is_equal(window)
# =============================================================================
# _user_adjusted guard (PR #188 review) — the flag exists so auto-fit NEVER
# fights a manually-adjusted view. The one branch that makes that true
# (resize while user-adjusted) had no coverage; both directions pinned here,
# driving the REAL _gui_input path (synthetic events), not the flag directly.
# =============================================================================
func _drag_viewer(v: AtlasWindowViewer, from: Vector2, to: Vector2) -> void:
var down := InputEventMouseButton.new()
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
down.position = from
down.global_position = from
v._gui_input(down)
var move := InputEventMouseMotion.new()
move.position = to
move.global_position = to
v._gui_input(move)
var up := InputEventMouseButton.new()
up.button_index = MOUSE_BUTTON_LEFT
up.pressed = false
up.position = to
up.global_position = to
v._gui_input(up)
func test_resize_after_manual_drag_keeps_user_view() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1280.0, 720.0)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(10, 20), 32)
_drag_viewer(v, Vector2(600.0, 400.0), Vector2(540.0, 380.0))
var user_zoom: float = v.get_view_zoom()
var user_offset: Vector2 = v.get_view_offset()
v.size = Vector2(1600.0, 900.0)
v.notification(Control.NOTIFICATION_RESIZED)
assert_float(v.get_view_zoom()).override_failure_message(
"resize while user-adjusted must NOT re-fit — zoom belongs to the user"
).is_equal_approx(user_zoom, 0.0001)
assert_vector(v.get_view_offset()).override_failure_message(
"resize while user-adjusted must NOT re-center — offset belongs to the user"
).is_equal_approx(user_offset, Vector2(0.001, 0.001))
func test_resize_without_user_adjustment_refits() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1280.0, 720.0)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(10, 20), 32)
var fitted_zoom: float = v.get_view_zoom()
v.size = Vector2(640.0, 360.0)
v.notification(Control.NOTIFICATION_RESIZED)
assert_float(v.get_view_zoom()).override_failure_message(
"resize with no manual adjustment must re-fit to the new viewport"
).is_not_equal(fitted_zoom)
+7 -3
View File
@@ -821,9 +821,13 @@ fn normalize_window_center(params: &BodyParams, center: DistrictPos) -> District
/// rather than only inside the former miss-branch: normalization needs /// rather than only inside the former miss-branch: normalization needs
/// `body_radius_km` to compute the wrap/clamp bounds, and it must happen /// `body_radius_km` to compute the wrap/clamp bounds, and it must happen
/// before either key exists, or an insane request and its sane normalized /// before either key exists, or an insane request and its sane normalized
/// twin would land in different cache entries / coalesce independently /// twin would land in different cache entries (exactly the bug this fix
/// (exactly the bug this fix closes — a garbage `window_center` was cached /// closes — a garbage `window_center` was cached standalone instead of
/// standalone instead of collapsing onto its valid twin). The one-time cost /// collapsing onto its valid twin). The coalescing key itself is
/// `(ConnectionId, body_id)` — it never carried `center`, so coalescing
/// was never at risk of diverging per-center; normalizing before
/// `submit_window` matters only so the work item DERIVES (and echoes) the
/// canonical center. The one-time cost
/// (a single indexed `bodies` row read) is paid on every window request now, /// (a single indexed `bodies` row read) is paid on every window request now,
/// not just on a cache miss — a request whose normalized center hits the /// not just on a cache miss — a request whose normalized center hits the
/// cache still needed this read to know WHICH key to check. /// cache still needed this read to know WHICH key to check.