## T-1138 (D-226 T-1124 amendment §1-§5): tests for AtlasWindowViewer + its ## companion request/cache orchestration (atlas_window_request.gd) — pure ## logic against hand-built AtlasLayerResponse-shaped dicts, matching the ## ticket's "unit tests against hand-built response dicts" instruction. Live ## end-to-end verification against a real spawned server is separate ## (companion-run evidence, not gdUnit — this file never touches SimBridge's ## live-mode path, only the response-handling/cache/overlay logic that path ## eventually feeds). class_name TestAtlasWindowViewer extends GdUnitTestSuite # atlas_window_request.gd has no class_name (review #8 precedent throughout # this cluster) — preloaded once here, not re-load()ed per test (gdlint # duplicated-load). const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd") # T-1142: district_extent()/canonicalize_district_center() — used to derive # real (cols/rows_half) bounds for the wrap/pole-wall tests below. const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd") ## Build a hand-authored DistrictWindowLayer dict (n=2, matching the shape ## district_grid/region_grid fixtures already use elsewhere in this suite). static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary: return { "center": [center.x, center.y], "n": n, "morphology": PackedByteArray([8, 14, 0, 1]), "elev_q": PackedByteArray([40, 90, 5, 60]), "temp_dc": [120, 95, -32768, 60], "moisture_q": PackedByteArray([50, 30, 90, 20]), "vegetation": PackedByteArray([2, 1, 6, 3]), "glaciation": PackedByteArray([0, 0, 1, 2]), } static func _mock_response(body_id: String, window: Variant) -> Dictionary: return {"body_id": body_id, "status": "Ready", "district_window": window} # ============================================================================= # AtlasWindowViewer — entry + overlay defs # ============================================================================= func test_enter_with_no_response_leaves_window_null_and_pending() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20)) assert_that(v.get_district_window()).is_null() ## Feeding a matching Ready response (via the SAME SimBridge.atlas_layers_received ## routing path the viewer subscribes to in _ready()) must populate the window. func test_enter_then_matching_response_populates_window() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2) var window: Dictionary = _mock_window(Vector2i(10, 20), 2) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) assert_that(v.get_district_window()).is_equal(window) ## A response for a DIFFERENT body must not populate the window — the ## body_id scoping AtlasWindowRequest.on_response() checks. func test_response_for_different_body_is_ignored() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2) var window: Dictionary = _mock_window(Vector2i(10, 20), 2) SimBridge.atlas_layers_received.emit(_mock_response("GJ_wrong_body", window)) assert_that(v.get_district_window()).is_null() ## A response whose echoed (center, n) does NOT match what was last asked for ## is stale — §2's race-condition guard. Simulates a superseded-by-a-later-pan ## response arriving after the fact. func test_response_with_mismatched_echo_is_discarded_as_stale() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2) var stale_window: Dictionary = _mock_window(Vector2i(99, 99), 2) # wrong center SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", stale_window)) assert_that(v.get_district_window()).is_null() ## §1: an as-yet-underived window rides as `district_window: None` inside a ## Ready response — this is NOT an error, the viewer just keeps waiting ## (get_district_window() stays null, no crash, no window content shown). func test_ready_response_with_null_district_window_keeps_waiting() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", null)) assert_that(v.get_district_window()).is_null() func test_overlay_defs_include_the_three_toggle_ids() -> void: var ids: Array = [] for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS: ids.append(d["id"]) assert_that(ids).contains(["gen_dw_temp", "gen_dw_moisture", "gen_dw_veg"]) ## Glaciation is explicitly NOT a toggle id (§5: "an always-on modifier, not ## a toggle") — a regression here would silently re-introduce it as a switch. func test_overlay_defs_do_not_include_glaciation() -> void: var ids: Array = [] for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS: ids.append(d["id"]) assert_that(ids).not_contains(["gen_dw_glaciation", "gen_dw_ice"]) func test_set_overlay_visible_toggles_and_is_overlay_visible_reflects_it() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) assert_bool(v.is_overlay_visible("gen_dw_temp")).is_false() v.set_overlay_visible("gen_dw_temp", true) assert_bool(v.is_overlay_visible("gen_dw_temp")).is_true() func test_set_overlay_visible_unknown_id_is_a_noop() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.set_overlay_visible("not_a_real_overlay", true) assert_bool(v.is_overlay_visible("not_a_real_overlay")).is_false() # ============================================================================= # T-1120 capture-API parity (the ticket's explicit note: must survive here too) # ============================================================================= func test_set_view_and_getters_round_trip() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.set_view(2.5, Vector2(30.0, -10.0)) assert_that(v.get_view_zoom()).is_equal_approx(2.5, 0.001) assert_that(v.get_view_offset()).is_equal(Vector2(30.0, -10.0)) func test_set_view_clamps_to_min_max_zoom() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.set_view(0.01, Vector2.ZERO) assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.001) v.set_view(1000.0, Vector2.ZERO) assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MAX_ZOOM, 0.001) # ============================================================================= # AtlasWindowRequest — cache reuse (§4's Esc-then-re-enter / pan-back hit) # ============================================================================= func test_window_request_cache_hit_emits_synchronously_no_pending() -> void: var owner_stub := RefCounted.new() var req = auto_free(AtlasWindowRequest.new(owner_stub)) add_child(req) # Prime the cache directly (bypassing the network path) — the ticket's # own instruction: unit test against hand-built response dicts. req.get_cache().put("GJ380c", Vector2i(1, 1), 2, _mock_window(Vector2i(1, 1), 2)) var received: Array = [] req.window_ready.connect(func(w: Dictionary) -> void: received.append(w)) req.request_now("GJ380c", Vector2i(1, 1), 2) assert_int(received.size()).is_equal(1) assert_bool(req.is_pending()).override_failure_message( "a cache hit must never leave the request pending" ).is_false() func test_window_request_cache_miss_leaves_pending_true() -> void: var owner_stub := RefCounted.new() var req = auto_free(AtlasWindowRequest.new(owner_stub)) add_child(req) req.request_now("GJ380c", Vector2i(5, 5), 2) assert_bool(req.is_pending()).is_true() ## on_response() with a matching Ready+window response resolves the pending ## request AND populates the cache — verified by a second request_now() call ## for the same (center, n) becoming a cache hit with zero additional pending. func test_on_response_resolves_and_populates_cache_for_next_request() -> void: var owner_stub := RefCounted.new() var req = auto_free(AtlasWindowRequest.new(owner_stub)) add_child(req) req.request_now("GJ380c", Vector2i(2, 2), 2) assert_bool(req.is_pending()).is_true() var window: Dictionary = _mock_window(Vector2i(2, 2), 2) req.on_response(_mock_response("GJ380c", window)) assert_bool(req.is_pending()).is_false() # Re-request the SAME (body, center, n) — must be a cache hit, no pending. req.request_now("GJ380c", Vector2i(2, 2), 2) assert_bool(req.is_pending()).override_failure_message( "a second request for an already-resolved window must hit the cache" ).is_false() # ============================================================================= # T-1142 item 2: fit-and-center on entry (Jeroen's "postage stamp" finding) # ============================================================================= ## enter() must fit-and-center, NOT reset to the old zoom=1.0/offset=ZERO. ## With a real viewport size set on the Control, the fitted zoom for an ## n=32 default window must scale up past 1.0 (matches ## test_atlas_window_geometry.gd's own fit math, exercised here through the ## real enter() call path instead of the pure function directly). func test_enter_fits_and_centers_instead_of_resetting_to_zoom_one() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 32) assert_float(v.get_view_zoom()).override_failure_message( "an n=32 (512px native) composite in a 1920x1080 viewport must be fitted" + " (zoom > 1.0), not left at the old zoom=1.0 postage-stamp default" ).is_greater(1.0) ## After enter()'s fit, the offset must not be Vector2.ZERO (the old ## behavior) — it must be the CENTERING offset the fit produces. func test_enter_offset_is_not_the_old_zero_default() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 32) assert_that(v.get_view_offset()).override_failure_message( "a fitted+centered composite in a 1920x1080 viewport should not sit at (0,0)" ).is_not_equal(Vector2.ZERO) # ============================================================================= # T-1142 item 3: header carries the body's proper name (cheap half of T-1141) # ============================================================================= func test_header_location_label_includes_body_proper_name() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.enter({"body_id": "GJ380c", "proper_name": "Lendel"}, {}, Vector2i(5, 5), 2) assert_str(v._location_label()).contains("Lendel") ## No proper_name on the body dict -> falls back to body_id (matches ## AtlasViewer's own _refresh_screen_header fallback chain exactly). func test_header_location_label_falls_back_to_body_id() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.enter({"body_id": "GJ903b"}, {}, Vector2i(5, 5), 2) assert_str(v._location_label()).contains("GJ903b") func test_header_location_label_still_includes_the_coordinates() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.enter({"body_id": "GJ380c", "proper_name": "Lendel"}, {}, Vector2i(42, -7), 2) var label: String = v._location_label() assert_str(label).contains("42") assert_str(label).contains("-7") # ============================================================================= # T-1142 item 4: drag-pan through the real input chain (DistrictScreen -> # AtlasWindowViewer._gui_input) — verifies no ancestor eats the event. # ============================================================================= ## Drives _gui_input DIRECTLY on the viewer (the same call gdUnit's own ## headless-mode InputEvent limitation forces every other _gui_input test in ## this cluster to use — see test_atlas_descend_entry.gd's own note on real ## mouse events not being transported in headless mode). This confirms the ## HANDLER logic itself moves _view_offset on a drag; the "does the Control ## TREE deliver the event to this handler at all" question is the separate, ## real concern item 4 raises (DistrictScreen's own mouse_filter=STOP could ## theoretically intercept) — that is checked by the follow-up test below, ## which drives the SAME sequence starting from DistrictScreen's root. func test_drag_pan_moves_view_offset() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32) # body_radius_km absent -> no pole wall (identity clamp), isolating the # drag-delta math itself from item 5's clamp in this test. var offset_before: Vector2 = v.get_view_offset() var press := InputEventMouseButton.new() press.button_index = MOUSE_BUTTON_LEFT press.pressed = true press.position = Vector2(400.0, 300.0) v._gui_input(press) var motion := InputEventMouseMotion.new() motion.position = Vector2(500.0, 350.0) # +100, +50 drag delta v._gui_input(motion) assert_that(v.get_view_offset()).override_failure_message( "a drag must move _view_offset away from its pre-drag value" ).is_not_equal(offset_before) assert_that(v.get_view_offset()).is_equal(offset_before + Vector2(100.0, 50.0)) ## The real scene-tree path: DistrictScreen (mouse_filter=STOP, no ## _gui_input override) -> AtlasWindowViewer (mouse_filter=STOP, HAS ## _gui_input). Godot delivers _gui_input to the DEEPEST/topmost Control ## under the mouse first — DistrictScreen having no _gui_input override ## means it never intercepts before AtlasWindowViewer gets the event; this ## test confirms that structurally by driving the event through ## DistrictScreen's own child and checking the SAME state change reaches ## AtlasWindowViewer, exactly as if the event had arrived organically through ## the real app -> nav-stack -> DistrictScreen chain. func test_drag_pan_reaches_viewer_through_district_screen_chain() -> void: var screen: DistrictScreen = auto_free(DistrictScreen.new()) add_child(screen) screen.enter({"body": {"body_id": "GJ380c"}, "district_center": Vector2i(0, 0)}) var offset_before: Vector2 = screen._viewer.get_view_offset() var press := InputEventMouseButton.new() press.button_index = MOUSE_BUTTON_LEFT press.pressed = true press.position = Vector2(400.0, 300.0) screen._viewer._gui_input(press) var motion := InputEventMouseMotion.new() motion.position = Vector2(460.0, 300.0) screen._viewer._gui_input(motion) assert_that(screen._viewer.get_view_offset()).override_failure_message( "a drag driven through DistrictScreen's child viewer must still move" + " _view_offset — no ancestor in the real screen chain eats the event" ).is_not_equal(offset_before) # ============================================================================= # T-1142 item 5: pole hard wall wired into the real drag handler # ============================================================================= ## A window already near the pole, dragged FAR toward it, must have its ## offset clamped by the real _gui_input path (not just the pure function in ## isolation — this confirms the wiring, not just the math). ## A synthetic small body (NOT GJ380c's real ~6238km radius) is used ## deliberately: with a real body's huge rows_half (~4785 for GJ380c), the ## wall sits so many screen-pixels away that even an "absurd" mouse-motion ## delta (bounded by a real screen's pixel dimensions) never reaches it — ## the wall is real but the test would need a physically-impossible mouse ## position to trigger it. A small synthetic radius (-> a small rows_half) ## keeps the wall reachable by an ordinary drag delta while exercising the ## exact same code path. func test_drag_pan_is_clamped_by_the_pole_wall_when_wired() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) v.size = Vector2(800.0, 800.0) # A tiny synthetic radius -> district_extent().rows_half is small (a few # hundred districts), so the pole wall is within reach of an ordinary # drag delta. Center 10 districts from the north pole. var radius_km := 50.0 var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km) var rows_half: int = int(extent["rows_half"]) v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(0, -rows_half + 10), 32) var press := InputEventMouseButton.new() press.button_index = MOUSE_BUTTON_LEFT press.pressed = true press.position = Vector2(400.0, 400.0) v._gui_input(press) var motion := InputEventMouseMotion.new() motion.position = Vector2(400.0, -50000.0) # an absurd upward drag v._gui_input(motion) assert_float(v.get_view_offset().y).override_failure_message( "an absurd drag toward the pole must be clamped by the real input path" ).is_greater(-50000.0) # ============================================================================= # T-1142 item 6: east-west wrap — canonicalization on entry + cache reuse # ============================================================================= ## enter() canonicalizes an out-of-range center BEFORE it becomes ## _held_center — a column past the body's circumference wraps into range. func test_enter_canonicalizes_an_out_of_range_center() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) var radius_km := 6371.0 var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km) var cols: int = int(extent["cols"]) v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(cols + 50, 0), 32) var window: Dictionary = _mock_window(Vector2i(50, 0), 32) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) assert_that(v.get_district_window()).override_failure_message( "the response must be adopted under the CANONICALIZED center (50, 0)," + " matching what the server would echo back for the wrapped request" ).is_equal(window) ## A center ONE column past the seam (item 6b): the SAME cache key as its ## twin at column 0 — a full-circumnavigation pan back to the seam must hit ## cache, not re-derive, because both requests canonicalize to the same ## (body, center, n) key. func test_center_one_column_past_the_seam_shares_a_cache_key_with_its_twin() -> void: var radius_km := 6371.0 var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km) var cols: int = int(extent["cols"]) var v1: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v1) v1.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(cols, 50), 32) var v2: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v2) v2.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(0, 50), 32) # Both must adopt the SAME server response (keyed on the same # canonicalized center) — proves the cache key (and the outbound # request) canonicalize identically for the seam and its twin. var window: Dictionary = _mock_window(Vector2i(0, 50), 32) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) assert_that(v1.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)