## T-1182 tests: StepCanvasViewer — the rung transport state machine ## (enter() lands on the Global opener, scroll steps through the ladder, ## overlay toggle wiring, pan-edge re-request, edge-scroll) and ## RegionalScreen's re-entry guard against the new viewer. test_mode ## (SimBridge default outside SR_LIVE=1) means request_step_canvas() is a ## silent no-op — these tests exercise client-side state only, no live ## server needed. class_name TestStepCanvasViewer extends GdUnitTestSuite const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd") ## GJ1c's own region-grid shape from the T-1183 eyeball (177x88) — reused ## across the T-1189/T-1192 section below so every test is grounded in the ## actual regression captured in ## .cache/screenshots/t1183-eyeball-run2/02-region.png. const GJ1C_GLOBAL_EXTENT := Vector2i(177, 88) ## Isolated Tier-2/3 disk-cache root for every directly-constructed viewer ## (T-1193 first slice). The default user://atlas_cache/ is SHARED across the ## whole machine — every worktree's gate run, live capture driver, and real ## play session writes the same directory — and T-1183's disk-cache lookup ## short-circuits BEFORE test_mode's silent-no-op IPC, so a warm shared cache ## delivers real canvases into tests written against "nothing ever arrives" ## (2026-07-25: a concurrent live GJ380c Global capture flipped the two ## before-any-canvas tests in another worktree's gate run). In test_mode the ## isolated root stays empty forever: no canvas ever arrives, so the cache ## never writes. const TEST_DISK_CACHE_ROOT := "user://test_step_canvas_viewer_cache/" func _make_viewer() -> StepCanvasViewer: var v: StepCanvasViewer = auto_free(StepCanvasViewer.new()) v.disk_cache_root_override = TEST_DISK_CACHE_ROOT return v func test_enter_lands_on_the_global_opener() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL) func test_get_body_id_reflects_the_entered_body() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) assert_str(v.get_body_id()).is_equal("") v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) assert_str(v.get_body_id()).is_equal("GJ380c") ## Scrolling one notch descends the ladder — cursor-anchored, so a cursor ## position must be supplied; the rung index advances by exactly one. func test_scroll_rung_descends_one_notch() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._scroll_rung(1, Vector2(400.0, 300.0)) assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION) func test_scroll_rung_clamps_at_the_deepest_rung() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) for _i in range(10): v._scroll_rung(1, Vector2(400.0, 300.0)) assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_CHUNK) func test_reset_to_global_returns_from_a_deep_rung() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._scroll_rung(1, Vector2(400.0, 300.0)) v._scroll_rung(1, Vector2(400.0, 300.0)) v._reset_to_global() assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL) ## PR #203 review (Hoshe finding 2): the hard full-zoom-out reset — a ## scroll-out gesture while ALREADY at Global (rung 0), with the view ## drifted from the canonical un-panned frame, must snap the view back to ## center (Jeroen's explicit HARD condition, carried from the retired ## viewer's own _maybe_reset_to_canonical_frame()). Behavioral, through the ## real input entry point (_scroll_rung with direction=-1), not a direct ## _reset_to_global() call — this is what would have caught the dead-code ## regression (scroll_step() clamping at index 0 meant _scroll_rung() ## returned before ever reaching a reset call). func test_scroll_out_at_global_after_a_pan_resets_the_view() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL) v._apply_pan_delta(Vector2(1.0, 0.0), 1.0) # drift the view off-center assert_bool(v._is_global_view_drifted()).override_failure_message( "test setup: a pan at Global must actually drift the view" ).is_true() v._scroll_rung(-1, Vector2(400.0, 300.0)) # scroll OUT — already at rung 0 assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL) assert_bool(v._is_global_view_drifted()).override_failure_message( "a scroll-out past the top of the ladder must hard-reset the drifted" + " Global view back to its canonical (centered) frame" ).is_false() ## The inverse guard: scrolling out while ALREADY at the canonical ## (un-drifted) Global frame must stay a no-op — the reset is edge-triggered ## on genuine drift, not a per-scroll unconditional reset. func test_scroll_out_at_undrifted_global_is_a_no_op() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._scroll_rung(-1, Vector2(400.0, 300.0)) assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL) assert_bool(v._is_global_view_drifted()).is_false() func test_overlay_visibility_defaults_to_off_for_every_toggle() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) for def: Dictionary in v.get_overlay_defs(): assert_bool(v.is_overlay_visible(def["id"])).is_false() func test_set_overlay_visible_updates_state() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) 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_no_op() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.set_overlay_visible("not_a_real_overlay", true) assert_bool(v.is_overlay_visible("not_a_real_overlay")).is_false() # ============================================================================= # RegionalScreen re-entry guard (BUG 2 lineage, carried forward from the # retired AtlasWindowViewer-era regression) — now against StepCanvasViewer. # ============================================================================= func test_regional_screen_repeat_enter_for_the_same_body_is_a_no_op() -> void: var screen: RegionalScreen = auto_free(RegionalScreen.new()) add_child(screen) var body: Dictionary = {"body_id": "GJ380c", "body_radius_km": 6238.4} screen.enter({"body": body, "system": {}}) screen._viewer._scroll_rung(1, Vector2(400.0, 300.0)) assert_str(screen._viewer.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION) screen.enter({"body": body, "system": {}}) # A no-op re-entry must NOT reset the held rung back to Global — that # would be the exact "repeat enter tears down in-flight state" class the # retired viewer's own cold-start guard existed to prevent. assert_str(screen._viewer.get_held_rung()).override_failure_message( "a repeat enter() for the SAME body must not reset the held rung" ).is_equal(StepCanvasTransport.RUNG_REGION) func test_regional_screen_different_body_still_re_enters() -> void: var screen: RegionalScreen = auto_free(RegionalScreen.new()) add_child(screen) screen.enter({"body": {"body_id": "GJ380c", "body_radius_km": 6238.4}, "system": {}}) screen.enter({"body": {"body_id": "OtherBody", "body_radius_km": 100.0}, "system": {}}) assert_str(screen._viewer.get_body_id()).is_equal("OtherBody") ## Relocated from test_atlas_descend_entry.gd (T-1182 PR #203 review — the ## AtlasViewer cluster orphan retirement, Tyre finding). This is the one live ## regression guard from that suite: RegionalScreen must wrap StepCanvasViewer ## (the stepped ladder), never fall back to the now-deleted AtlasViewer ## heightmap-texture display — a direct type-identity check, distinct from ## the behavioral tests above (which would only fail indirectly, via a ## missing method, if this ever regressed). func test_regional_screen_wraps_step_canvas_viewer_not_atlas_viewer() -> void: var screen: RegionalScreen = auto_free(RegionalScreen.new()) add_child(screen) assert_object(screen._viewer).override_failure_message( "RegionalScreen must wrap StepCanvasViewer (the stepped ladder) since T-1182," + " not the retired AtlasViewer heightmap-texture display" ).is_instanceof(StepCanvasViewer) # ============================================================================= # PR #203 review (Hoshe notes): pan-edge re-request (_maybe_refloat) and # edge-scroll pan — previously untested. _maybe_refloat() only does anything # once the terrain layer holds a real texture (get_footprint_px() is # ZERO/inert until then), so these tests drive StepCanvasTerrainLayer. # rebuild_from_canvas() directly (bypassing the network — a decoded canvas # dict is all it needs) to put the viewer into the "holding a real canvas" # state _maybe_refloat's early-out guards against. # ============================================================================= static func _synthetic_canvas(width: int, height: int) -> Dictionary: return { "width": width, "height": height, "morphology": null, "elev_q": null, "moisture_q": null, "vegetation": null, "glaciation": null, "temp_dc": [], "settlement_id": [], "courses": [], "cliffs": [], } func test_maybe_refloat_is_inert_before_any_canvas_has_arrived() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._scroll_rung(1, Vector2(400.0, 300.0)) # District — footprint still ZERO, nothing arrived var world_center_before: Vector2 = v._world_center v._maybe_refloat() assert_that(v._world_center).is_equal(world_center_before) ## A small pan (well under half the canvas footprint) must NOT re-float — ## the held canvas keeps drawing, no re-request (§4/§5's "only when a pan ## carries the view past the held window's edge"). func test_maybe_refloat_does_not_refloat_on_a_small_pan() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(800.0, 600.0) # a real viewport size — _maybe_refloat's # drift math is relative to get_rect().size's own center; leaving this at # the default ZERO would make screen_center ZERO too, so even a tiny # view_offset reads as "drifted past the canvas's own half-footprint" # (drift = view_offset + half, threshold = half*0.5) — a test-harness # artifact, not the behavior under test. v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._scroll_rung(1, Vector2(400.0, 300.0)) # District v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(64, 64), v.get_held_rung(), "") # _scroll_rung() re-centers view_offset to ZERO on arrival, which under a # real 800x600 viewport already puts the canvas center near screen center # (both small relative to the viewport) — center the canvas explicitly so # "small pan" starts from a known-centered baseline. v._view_offset = v.size * 0.5 - v._terrain_layer.get_footprint_px() * 0.5 var world_center_before: Vector2 = v._world_center v._view_offset += Vector2(2.0, 0.0) # tiny drift, far under half the footprint v._maybe_refloat() assert_that(v._world_center).override_failure_message( "a small pan must not re-float the held canvas" ).is_equal(world_center_before) ## A large pan (past half the canvas footprint) DOES re-float — new ## world_center, view_offset reset to ZERO (the canvas re-centers under the ## new request). func test_maybe_refloat_refloats_once_the_pan_crosses_the_edge_threshold() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._scroll_rung(1, Vector2(400.0, 300.0)) # District v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(64, 64), v.get_held_rung(), "") var world_center_before: Vector2 = v._world_center var footprint: Vector2 = v._terrain_layer.get_footprint_px() v._view_offset = Vector2(footprint.x, 0.0) # far past half the footprint v._maybe_refloat() assert_that(v._world_center).override_failure_message( "a pan past the edge threshold must re-float (new world_center)" ).is_not_equal(world_center_before) assert_that(v._view_offset).override_failure_message( "re-floating resets view_offset to ZERO (the canvas re-centers)" ).is_equal(Vector2.ZERO) ## Global never re-floats on pan (D-255(a): its canvas is the whole body, ## no edge to cross) — even with a real texture held and a huge drift. func test_maybe_refloat_is_a_no_op_at_global_rung() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(200, 100), v.get_held_rung(), "") var world_center_before: Vector2 = v._world_center v._view_offset = Vector2(9_999.0, 9_999.0) v._maybe_refloat() assert_that(v._world_center).is_equal(world_center_before) # ============================================================================= # Edge-scroll: suppression conditions + direction. # ============================================================================= func test_edge_scroll_suppressed_without_application_focus() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(800.0, 600.0) v._app_has_focus = false v._last_mouse_pos = Vector2(2.0, 300.0) # well inside the edge margin assert_bool(v._is_cursor_edge_scrolling()).is_false() func test_edge_scroll_suppressed_when_cursor_has_never_moved_over_the_control() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(800.0, 600.0) # _last_mouse_pos defaults to (-1, -1) — an impossible in-bounds position, # so edge-scroll never fires before the mouse has moved over the control # at least once (matches the retired viewer's own documented contract). assert_bool(v._is_cursor_edge_scrolling()).is_false() func test_edge_scroll_active_near_the_left_edge() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(800.0, 600.0) v._app_has_focus = true v._last_mouse_pos = Vector2(2.0, 300.0) assert_bool(v._is_cursor_edge_scrolling()).is_true() var direction: Vector2 = v._edge_scroll_direction() assert_float(direction.x).is_less(0.0) assert_float(direction.y).is_equal(0.0) func test_edge_scroll_active_near_the_right_edge() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(800.0, 600.0) v._app_has_focus = true v._last_mouse_pos = Vector2(798.0, 300.0) var direction: Vector2 = v._edge_scroll_direction() assert_float(direction.x).is_greater(0.0) func test_edge_scroll_inactive_well_inside_the_viewport() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(800.0, 600.0) v._app_has_focus = true v._last_mouse_pos = Vector2(400.0, 300.0) # dead center — far from any edge assert_bool(v._is_cursor_edge_scrolling()).is_false() # ============================================================================= # T-1183: disk-cache sweep-trigger wiring # ============================================================================= ## enter() must invoke the disk cache's (2a) visit sweep for the entered ## body — a smoke test that the wiring exists and doesn't crash; the sweep ## LOGIC itself (what gets evicted and why) is covered exhaustively by ## test_step_canvas_disk_cache.gd. Uses a distinctive body_id with nothing ## ever cached under it, so the sweep is a true no-op read (no writes to the ## real user://atlas_cache/ directory this test could leak). func test_enter_runs_the_disk_cache_visit_sweep_without_crashing() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "T1183_sweep_smoke_test_body", "body_radius_km": 6238.4}, {}) # If the wiring is broken (e.g. calling a method that doesn't exist), the # enter() call itself would already have failed above — reaching here # with the expected held rung is the assertion. assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL) ## The coarse background sweep timer exists, is not per-frame (a real ## Timer node, not a _process()-driven counter), autostarts, and is set to ## the documented coarse interval — never a sub-frame or per-frame value. func test_disk_sweep_timer_is_coarse_and_autostarts() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) var timer: Timer = v.get_node("DiskSweepTimer") assert_object(timer).is_not_null() # Godot resets Timer.autostart to false once the timer has actually # started after entering the tree (documented engine behavior — the flag # is a one-shot "start me on _ready()" instruction, not a persistent # state mirror). The real behavioral guarantee is "the timer is running, # unpaused, without anyone having to call start() explicitly" — # is_stopped() == false is the correct read of that. assert_bool(timer.is_stopped()).override_failure_message( "the disk sweep timer must autostart running, no explicit start() call needed" ).is_false() assert_float(timer.wait_time).override_failure_message( "the disk sweep timer must be coarse (minutes), never a per-frame interval" ).is_greater_equal(60.0) ## The timer's timeout must actually route to the disk cache's ## run_background_sweep() for the currently-entered body — verified by ## invoking the private handler directly (the same "call the handler, don't ## wait on a real Timer" pattern used elsewhere in this cluster for ## non-blocking test speed) against an injected-root request so this test ## touches no real cache files. func test_disk_sweep_timeout_handler_runs_background_sweep_for_the_current_body() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "T1183_sweep_smoke_test_body", "body_radius_km": 6238.4}, {}) # No live server, no cached entries for this body — the assertion is # that calling the handler does not crash and leaves the (empty) cache # consistent, mirroring the enter()-sweep smoke test above. v._on_disk_sweep_timeout() assert_int(v.get_request().get_disk_cache().entry_count("T1183_sweep_smoke_test_body")).is_equal(0) # ============================================================================= # T-1189: extent cap wired end-to-end (viewer -> transport), plus the # cache-key consistency the ticket calls out explicitly ("capping happens # BEFORE the request is issued so keys stay consistent"). No live server — # a Global canvas is landed via the SAME Tier-1 cache-hit path # StepCanvasRequest's own tests use (get_cache().put() + request_now()'s # synchronous cache-hit emit), so the full _on_canvas_ready wiring runs for # real rather than being shortcut. # ============================================================================= ## Land a Global canvas of the given size into the viewer's OWN cache (Tier ## 1), then fire the request that the real cache-hit path serves ## synchronously — same mechanism test_step_canvas_request.gd's own ## cache-hit tests use, now driven through the viewer so _on_canvas_ready() ## and _global_body_extent actually populate through the real signal wiring. static func _land_global_canvas(v: StepCanvasViewer, width: int, height: int) -> void: var req: Variant = v.get_request() req.get_cache().put( v.get_body_id(), "Global", Vector2i.ZERO, Vector2i.ZERO, TestStepCanvasViewer._synthetic_canvas(width, height) ) v._fire_request() # Global's own request — served from the cache hit just landed func test_global_canvas_arrival_populates_the_body_extent_cap_source() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) assert_that(v._global_body_extent).is_equal(GJ1C_GLOBAL_EXTENT) ## The T-1183 eyeball regression itself: once the Global echo has landed, ## scrolling to Region and firing its request must produce a CAPPED extent ## — never the raw viewport-fit 384x216 that overran the body on both axes. func test_region_request_extent_is_capped_to_the_landed_global_extent() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) # the T-1183 eyeball's own viewport v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) v._scroll_rung(1, Vector2(960.0, 540.0)) # descend to Region — fires the capped request var extent: Vector2i = v._request_extent() assert_int(extent.x).override_failure_message( "Region's requested extent must never exceed the body's own region-grid width" ).is_less_equal(GJ1C_GLOBAL_EXTENT.x) assert_int(extent.y).override_failure_message( "Region's requested extent must never exceed the body's own region-grid height" ).is_less_equal(GJ1C_GLOBAL_EXTENT.y) assert_that(extent).is_equal(GJ1C_GLOBAL_EXTENT) # 1920x1080 viewport-fit exceeds 177x88 on both axes ## Cold-start fallback (documented on StepCanvasViewer._request_extent()): ## before ANY Global response has landed, `_global_body_extent` is still ## ZERO — the Region request must go out UNCAPPED (server clamps ## independently) rather than silently collapsing to a zero-cell request. func test_region_request_extent_is_uncapped_before_the_global_echo_lands() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) # No _land_global_canvas() call — simulates scrolling in before the # FIRST (Global) request's response has arrived. v._scroll_rung(1, Vector2(960.0, 540.0)) var extent: Vector2i = v._request_extent() var uncapped: Vector2i = StepCanvasTransport.viewport_fit_extent(v.size, "Region") assert_that(extent).override_failure_message( "before the Global echo lands, the Region request must be the ordinary" + " uncapped viewport-fit extent, not silently zeroed" ).is_equal(uncapped) ## Cache-key discipline (T-1182/T-1183, ticket's own explicit call-out): the ## cap must be applied BEFORE _fire_request() builds the request, so the ## extent that becomes part of the cache key is the SAME capped value that ## gets served — a request for "the same spot" must always resolve to the ## same key, capped or not, never a key built from one extent and served ## under another. func test_capped_extent_matches_what_the_request_actually_sends() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) v._scroll_rung(1, Vector2(960.0, 540.0)) # Region — fires _fire_request() internally # _request_extent() is the SAME function _fire_request() calls to build # the outbound request/cache key — calling it again here must be # idempotent and match what was actually requested (no separate, # divergent cap path). var extent_now: Vector2i = v._request_extent() assert_that(extent_now).is_equal(GJ1C_GLOBAL_EXTENT) # The Tier-1 cache key StepCanvasCache builds from this SAME extent must # be a real, findable key once a response for it lands — proving the # capped extent (not the raw viewport-fit one) is what keys the cache. var req: Variant = v.get_request() var center: Vector2i = StepCanvasTransport.snap_to_gridunit(v._world_center, "Region") var canvas := TestStepCanvasViewer._synthetic_canvas(GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) req.get_cache().put("T1189_extent_letterbox_test_body", "Region", center, extent_now, canvas) assert_bool( req.get_cache().has("T1189_extent_letterbox_test_body", "Region", center, GJ1C_GLOBAL_EXTENT, 0) ).override_failure_message( "a cache entry stored under the CAPPED extent must be reachable" + " under that same capped extent — key consistency" ).is_true() ## Shape-generic guard: District's spacing differs from Global's, so its ## request extent must be completely unaffected by a landed Global canvas — ## proving the cap is spacing-keyed, not applied indiscriminately to every ## rung once a Global extent is known. func test_district_request_extent_is_never_capped_by_the_global_extent() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) v._scroll_rung(1, Vector2(960.0, 540.0)) # Region v._scroll_rung(1, Vector2(960.0, 540.0)) # District var extent: Vector2i = v._request_extent() var uncapped: Vector2i = StepCanvasTransport.viewport_fit_extent(v.size, "District") assert_that(extent).is_equal(uncapped) ## Hoshe (review round 2): _global_body_extent must NOT leak across a body ## switch — land a Global extent for body A, enter() body B, and prove BOTH ## that the cap source itself reads back ZERO for the new body AND that a ## Region request for body B before ITS OWN Global echo lands goes out ## UNCAPPED (never silently capped by body A's leftover grid). The reset ## already exists at StepCanvasViewer.enter() ("a new body has its own ## region grid") — this proves it, through the real enter()/land/enter() ## sequence rather than asserting the field directly only. func test_global_body_extent_resets_on_a_different_body_and_does_not_leak() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) assert_that(v._global_body_extent).override_failure_message( "test setup: body A must actually have a landed cap source" ).is_equal(GJ1C_GLOBAL_EXTENT) v.enter({"body_id": "T1189_extent_cap_body_b", "body_radius_km": 3000.0}, {}) assert_that(v._global_body_extent).override_failure_message( "entering a DIFFERENT body must reset the cap source to ZERO — body" + " A's region grid must never leak into body B's requests" ).is_equal(Vector2i.ZERO) v._scroll_rung(1, Vector2(960.0, 540.0)) # Region, for body B — no Global echo yet var extent: Vector2i = v._request_extent() var uncapped: Vector2i = StepCanvasTransport.viewport_fit_extent(v.size, "Region") assert_that(extent).override_failure_message( "body B's Region request, before body B's own Global echo has" + " landed, must go out UNCAPPED — never capped by body A's stale" + " leftover region-grid extent" ).is_equal(uncapped) # ============================================================================= # T-1189/T-1192: shared letterbox mechanism — centering + Global fit scale. # ============================================================================= ## T-1192's own headline defect: the Global canvas must no longer draw ## top-left-anchored at Vector2.ZERO — once a canvas lands, the viewer must ## have computed a non-zero centering offset (unless the canvas happens to ## exactly fill the viewport, not the case here). func test_global_canvas_arrival_centers_the_view_not_top_left() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) assert_that(v._view_offset).override_failure_message( "a landed Global canvas smaller than the viewport must be CENTERED," + " never left at the raw top-left Vector2.ZERO anchor" ).is_not_equal(Vector2.ZERO) ## D-255 amendment 2026-07-25 texel-exactness: the `_canvas.scale` value ## itself is NOT required to be a whole number (9/5 = 1.8 is entirely ## legitimate) — what MUST be exact is the resulting on-screen ## pixels-per-gridunit ratio. Verified end-to-end through the real viewer ## wiring: `_canvas_scale * base_display_ratio` (the rung's own display ## ratio, 5.0 for Global) must land on an exact integer, for both the ## legend-reserved 1920x1080 case (R=9, scale=1.8) AND the 4K case (R=20, ## scale=4.0) — the SAME formula, no separate coverage-threshold branch. func test_global_canvas_scale_yields_an_exact_integer_pixels_per_gridunit_ratio() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) assert_that(v._canvas.scale).is_equal(Vector2(v._canvas_scale, v._canvas_scale)) var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global") var effective_px_per_gridunit: float = v._canvas_scale * base_ratio assert_float(effective_px_per_gridunit).override_failure_message( "the FINAL on-screen pixels-per-gridunit ratio must be an exact" + " integer even when the _canvas.scale multiplier itself is not" ).is_equal_approx(roundf(effective_px_per_gridunit), 0.001) # The lead's own cited reference number for this exact scenario. assert_float(effective_px_per_gridunit).is_equal_approx(9.0, 0.001) ## Same invariant at a large (4K-class) viewport, where the integer ratio ## (20) happens to make the _canvas.scale multiplier itself a whole number ## too (20/5 = 4.0) — confirms the 1080p case above isn't a coincidence of ## a small viewport, just the same formula at a different achievable ratio. func test_global_canvas_scale_at_4k_also_yields_an_exact_integer_ratio() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(3840.0, 2160.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global") var effective_px_per_gridunit: float = v._canvas_scale * base_ratio assert_float(effective_px_per_gridunit).is_equal_approx(20.0, 0.001) ## Fixed rungs must NEVER receive the Global fit multiplier — `_canvas.scale` ## stays 1.0 once the player has descended past Global, even though a ## Global canvas was landed earlier in the same session. func test_fixed_rung_canvas_scale_stays_one_after_descending_from_global() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) v._scroll_rung(1, Vector2(960.0, 540.0)) # Region v._terrain_layer.rebuild_from_canvas( TestStepCanvasViewer._synthetic_canvas(200, 100), v.get_held_rung(), "" ) v._recompute_canvas_transform() assert_float(v._canvas_scale).is_equal_approx(1.0, 0.001) assert_that(v._canvas.scale).is_equal(Vector2.ONE) ## Legend non-overlap (T-1192: "lay the legend out beside the canvas... never ## over it"): the legend panel sits at a fixed left-column position ## (PANEL_MARGIN, ...) with a known width — once a Global canvas is landed ## and centered, its drawn rect's LEFT edge must be at or past the legend's ## own right edge, never underneath it. func test_global_canvas_left_edge_never_overlaps_the_legend_column() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) var canvas_left_edge: float = v._view_offset.x assert_float(canvas_left_edge).override_failure_message( "the Global canvas's drawn left edge must be at or past the reserved" + " legend column — the legend must never be covered by the map" ).is_greater_equal(StepCanvasTransport.LEGEND_COLUMN_PX - 0.01) ## Resize must re-fit/re-center a HELD canvas, not just a freshly-arriving ## one — _notification(NOTIFICATION_RESIZED) wires _recompute_canvas_transform() ## for exactly this case. func test_resize_recenters_an_already_held_global_canvas() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(1920.0, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) var offset_before: Vector2 = v._view_offset v.size = Vector2(1280.0, 720.0) v._notification(Control.NOTIFICATION_RESIZED) assert_that(v._view_offset).override_failure_message( "a resize must re-center the held canvas for the NEW viewport size" ).is_not_equal(offset_before) ## Hoshe (review round 2): a narrow viewport where GJ1c's canvas exceeds ## the available width on the gridunit lattice itself (177 gridunits > ## available px after the legend column is reserved) used to make the OLD ## fractional-fit branch return a sub-1x scale (0.39x) — a real downscale ## below native resolution, violating "never below native". The integer ## px/gridunit ratio floors at 1 instead: `_canvas_scale` must never drop ## under 1.0, end-to-end through the real viewer wiring, not just the pure ## transport function this mirrors. func test_global_canvas_scale_never_drops_below_native_on_a_narrow_viewport() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.size = Vector2(348.0 + StepCanvasTransport.LEGEND_COLUMN_PX, 1080.0) v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {}) TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y) var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global") assert_float(v._canvas_scale * base_ratio).override_failure_message( "the effective pixels-per-gridunit ratio must floor at 1 (native)," + " never a sub-1x downscale, even on a viewport this narrow" ).is_equal_approx(1.0, 0.001) # ============================================================================= # T-971 (AtlasAgentInterface): jump_to() — the fixed-center revisit seam — # and get_current_canvas_summary(). # ============================================================================= ## jump_to() must set the SAME held rung/world_center a cursor-anchored ## scroll to that same spot would land on — this is the "same cache key" ## guarantee AtlasAgentInterface's jump_to_center intent depends on ## (verified end-to-end, through act(), in test_atlas_agent_interface.gd; ## this is the narrower unit-level check directly against the viewer). func test_jump_to_sets_held_rung_and_world_center() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v.jump_to(Vector2(500.0, -250.0), StepCanvasTransport.RUNG_QUARTER) assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_QUARTER) assert_that(v.get_world_center()).is_equal(Vector2(500.0, -250.0)) ## Omitting `rung` keeps whatever rung is currently held — the "revisit ## within the same rung" common case shouldn't require repeating it. func test_jump_to_keeps_current_rung_when_omitted() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._scroll_rung(1, Vector2(400.0, 300.0)) # Region v.jump_to(Vector2(10.0, 20.0)) assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION) ## Jumping to Global always forces world_center to ZERO — Global has no ## panned-center concept (mirrors _scroll_rung()'s own Global-rung handling). func test_jump_to_global_forces_world_center_to_zero() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._scroll_rung(1, Vector2(400.0, 300.0)) # Region v.jump_to(Vector2(777.0, 888.0), StepCanvasTransport.RUNG_GLOBAL) assert_that(v.get_world_center()).is_equal(Vector2.ZERO) func test_jump_to_unrecognized_rung_is_a_no_op() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) var rung_before: String = v.get_held_rung() var center_before: Vector2 = v.get_world_center() v.jump_to(Vector2(1.0, 2.0), "NotARealRung") assert_str(v.get_held_rung()).is_equal(rung_before) assert_that(v.get_world_center()).is_equal(center_before) func test_get_current_canvas_summary_before_any_canvas_arrives() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) var summary: Dictionary = v.get_current_canvas_summary() assert_bool(summary.get("has_canvas", true)).is_false() assert_str(summary.get("rung", "")).is_equal(StepCanvasTransport.RUNG_GLOBAL) ## The T-1157-inventory-relevant correctness check: course/cliff/settlement ## counts must match a fixture canvas exactly, including the per-class ## course histogram and settlement id dedup (mirrors ## StepCanvasAnnotationLayer._draw_settlements()'s own dedup discipline — ## covering the same cell id twice must not double-count). func test_get_current_canvas_summary_counts_match_a_fixture_canvas() -> void: var v: StepCanvasViewer = _make_viewer() add_child(v) v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}) v._scroll_rung(1, Vector2(400.0, 300.0)) # Region var canvas: Dictionary = _synthetic_canvas(4, 4) canvas["courses"] = [ {"class": 0, "points": [[0.0, 0.0], [1.0, 1.0]]}, {"class": 0, "points": [[2.0, 2.0], [3.0, 3.0]]}, {"class": 2, "points": [[4.0, 4.0], [5.0, 5.0]]}, ] canvas["cliffs"] = [{"a": 1}, {"b": 2}] # 4x4 grid; ids 5 and 5 (repeat, same settlement footprint) dedup to one, # id 9 is a second distinct settlement, 0 is "no settlement" and ignored. canvas["settlement_id"] = [ 0, 5, 5, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, ] v._on_canvas_ready(canvas) var summary: Dictionary = v.get_current_canvas_summary() assert_bool(summary.get("has_canvas", false)).is_true() assert_int(summary.get("canvas_width", 0)).is_equal(4) assert_int(summary.get("canvas_height", 0)).is_equal(4) assert_int(summary.get("course_count", 0)).is_equal(3) assert_int(summary.get("cliff_count", 0)).is_equal(2) assert_int(summary.get("settlement_count", 0)).is_equal(2) var by_class: Dictionary = summary.get("course_count_by_class", {}) assert_int(int(by_class.get(0, 0))).is_equal(2) assert_int(int(by_class.get(2, 0))).is_equal(1)