Jeroen: 'that does not fit the viewport'. Correct — Global rendered a real map into roughly a quarter of the available area. _letterbox_scale_for() reserves LEGEND_COLUMN_PX before computing the Global fit, but _request_extent() sized the request against the FULL Control width. Because that fit is an INTEGER pixels-per-gridunit ratio, the disagreement does not degrade gracefully: at a 1920-wide window we asked for 960 gridunits but could only fit floor(1628/960) = 1 px each, so the canvas drew at HALF the intended scale with room to spare on every side. The request is now sized to the drawable area, so both sides agree: 814 gridunits at 2 px = 1628 px, plus the 292 px legend column = exactly 1920. Verified through the capture harness — canvas_scale went 0.5 -> 1.0. Only Global reserves the column, so only Global adjusts; the fixed rungs are untouched. Also corrects the regression test I wrote yesterday, which computed its expectation from the full viewport and so encoded the bug. Client suite 1830 / 1804 passed / 0 failed / 26 skipped. Co-Authored-By: Claude <noreply@anthropic.com>
1067 lines
50 KiB
GDScript
1067 lines
50 KiB
GDScript
## 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_hard_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)
|
|
|
|
|
|
## Pair session 2026-07-26 — the SOFT threshold: a pan that crosses it must
|
|
## NOT re-float on the spot. It schedules the shared settle timer and leaves
|
|
## the view alone, so a held edge-scroll keeps panning the current canvas
|
|
## smoothly and issues ONE request when it stops, instead of a request plus a
|
|
## view snap on every frame past the threshold (the old behavior).
|
|
func test_soft_pan_drift_schedules_the_settle_instead_of_refloating_now() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
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(512, 384), v.get_held_rung(), "")
|
|
v._view_offset = v._centered_view_offset()
|
|
|
|
# Drift to just past SOFT but well short of HARD.
|
|
var half: Vector2 = v._terrain_layer.get_footprint_px() * 0.5
|
|
var target: float = (
|
|
StepCanvasViewer.PAN_REFLOAT_SOFT_FRACTION + StepCanvasViewer.PAN_REFLOAT_HARD_FRACTION
|
|
) * 0.5
|
|
var screen_center: Vector2 = v.get_rect().size * 0.5
|
|
v._view_offset = screen_center - half + Vector2(half.x * target, 0.0)
|
|
|
|
var fraction: float = v._pan_drift_fraction()
|
|
assert_float(fraction).override_failure_message(
|
|
"test setup: drift must land between the soft and hard thresholds"
|
|
).is_between(StepCanvasViewer.PAN_REFLOAT_SOFT_FRACTION, StepCanvasViewer.PAN_REFLOAT_HARD_FRACTION)
|
|
|
|
var center_before: Vector2 = v._world_center
|
|
var offset_before: Vector2 = v._view_offset
|
|
v._maybe_refloat()
|
|
|
|
assert_that(v._world_center).override_failure_message(
|
|
"a soft-threshold pan must NOT re-float immediately"
|
|
).is_equal(center_before)
|
|
assert_that(v._view_offset).override_failure_message(
|
|
"a soft-threshold pan must not snap the view — the held canvas keeps panning"
|
|
).is_equal(offset_before)
|
|
assert_bool(v._refetch_settle_timer.is_stopped()).override_failure_message(
|
|
"a soft-threshold pan must schedule the shared settle timer"
|
|
).is_false()
|
|
|
|
|
|
## The inverse: panning back inside the soft threshold cancels the pending
|
|
## settle — the view no longer wants a different canvas, so the request that
|
|
## was about to go out must not.
|
|
func test_panning_back_inside_the_soft_threshold_cancels_the_pending_settle() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(512, 384), v.get_held_rung(), "")
|
|
|
|
v._refetch_settle_timer.start() # pretend a soft crossing already scheduled one
|
|
v._view_offset = v._centered_view_offset() # centred == zero drift
|
|
v._maybe_refloat()
|
|
|
|
assert_bool(v._refetch_settle_timer.is_stopped()).override_failure_message(
|
|
"drifting back inside the soft threshold must cancel the pending refetch"
|
|
).is_true()
|
|
|
|
|
|
## 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()
|
|
# Cache under the extent the viewer will actually REQUEST. This used to pass
|
|
# Vector2i.ZERO because Global's cache key collapsed the extent to a
|
|
# sentinel — correct while Global had exactly one possible size per body,
|
|
# wrong since the D-255 extent inversion made it viewport-sized (a stale
|
|
# sentinel entry answered every request forever, so a resize could never
|
|
# take effect). The centre stays ZERO: Global's canvas really is whole-body
|
|
# and origin-anchored, so the server genuinely ignores it.
|
|
req.get_cache().put(
|
|
v.get_body_id(),
|
|
"Global",
|
|
Vector2i.ZERO,
|
|
v._request_extent(),
|
|
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, restated for the post-inversion ladder
|
|
## (D-255 amendment, pair session 2026-07-26). It used to be fixed by CAPPING
|
|
## Region's extent to the body's region grid; the inversion removes the defect
|
|
## at its source instead. Region's cell count is now plain viewport-fit, and
|
|
## what bounds it to the body is its GROUND extent: the shorter viewport axis
|
|
## spans exactly one region, so the canvas cannot wrap the body however large
|
|
## the window is. Asserting the ground extent is the honest version of what
|
|
## the old cap was reaching for.
|
|
func test_region_request_covers_exactly_one_region_on_the_short_axis() -> 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
|
|
var extent: Vector2i = v._request_extent()
|
|
|
|
# No longer capped to the body grid — the cell count is the window.
|
|
assert_that(extent).is_equal(StepCanvasTransport.viewport_fit_extent(v.size, "Region"))
|
|
|
|
# ...and the ground it covers is one region across the short axis, which
|
|
# is what actually prevents the sideways-repeat / pole-smear defect.
|
|
var spacing: float = StepCanvasTransport.spacing_for_rung(
|
|
"Region", extent, v.get_body_radius_km()
|
|
)
|
|
var short_axis_m: float = spacing * float(mini(extent.x, extent.y))
|
|
assert_float(short_axis_m).override_failure_message(
|
|
"Region must span exactly one region cell on the short axis, got %f m" % short_axis_m
|
|
).is_equal_approx(StepCanvasTransport.RUNG_EXTENT_M["Region"], 0.01)
|
|
|
|
# The whole body is 40,030 km around; this canvas must be a small fraction
|
|
# of it, not a wrap-around.
|
|
var circumference_m: float = TAU * 6371.0 * 1000.0
|
|
assert_bool(spacing * float(extent.x) < circumference_m).override_failure_message(
|
|
"a Region canvas must never span more ground than the body has"
|
|
).is_true()
|
|
|
|
|
|
## 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),
|
|
## outliving the cap it was written for: the extent that becomes part of the
|
|
## cache key must be the SAME value the request actually carries — a request
|
|
## for "the same spot" must always resolve to the same key, never a key built
|
|
## from one extent and served under another. The inversion makes this MORE
|
|
## load-bearing, not less: the extent now also determines gridunit spacing, so
|
|
## a key/request divergence would mean a canvas served at the wrong scale
|
|
## rather than merely the wrong size.
|
|
func test_request_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(StepCanvasTransport.viewport_fit_extent(v.size, "Region"))
|
|
|
|
# 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
|
|
# extent that keys the cache is the one the request carries.
|
|
var req: Variant = v.get_request()
|
|
var center: Vector2i = StepCanvasTransport.snap_to_gridunit(
|
|
v._world_center, "Region", extent_now, v.get_body_radius_km()
|
|
)
|
|
var canvas := TestStepCanvasViewer._synthetic_canvas(extent_now.x, extent_now.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, extent_now, 0)
|
|
).override_failure_message(
|
|
"a cache entry stored under the REQUESTED extent must be reachable"
|
|
+ " under that same 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)
|
|
|
|
|
|
## EYEBALL REGRESSION (pair session 2026-07-26, Lendel): the Atlas opened on a
|
|
## Global map that was literally two cells — one green, one blue — stretched
|
|
## across the window, reporting 19,598 km/gridunit, exactly half the body's
|
|
## circumference. Cause: enter() fires its first request BEFORE this Control is
|
|
## laid out, and the not-laid-out size is not always exactly ZERO, so a few
|
|
## stray pixels sailed past the `== Vector2.ZERO` guard and asked for a 2x2
|
|
## gridunit canvas. Harmless while Global ignored the requested extent and took
|
|
## its cell counts from the body's region grid; load-bearing the moment the
|
|
## D-255 extent inversion made the request the canvas size.
|
|
func test_request_extent_ignores_a_not_yet_laid_out_viewport() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
for degenerate in [Vector2.ZERO, Vector2(4.0, 4.0), Vector2(1920.0, 2.0)]:
|
|
v.size = degenerate
|
|
var extent: Vector2i = v._request_extent()
|
|
# The fallback is sized to the DRAWABLE area, so Global's reserved
|
|
# legend column comes off the width first — the request must match what
|
|
# can actually be drawn, or the Global integer fit drops a whole step
|
|
# and the map renders at half size in a window with room to spare.
|
|
var drawable := Vector2(
|
|
StepCanvasViewer.FALLBACK_VIEWPORT_PX.x - StepCanvasTransport.LEGEND_COLUMN_PX,
|
|
StepCanvasViewer.FALLBACK_VIEWPORT_PX.y
|
|
)
|
|
var expected: Vector2i = StepCanvasTransport.viewport_fit_extent(
|
|
drawable, v.get_held_rung()
|
|
)
|
|
assert_that(extent).override_failure_message(
|
|
"a %s viewport must fall back, not be taken literally — got %s" % [degenerate, extent]
|
|
).is_equal(expected)
|
|
|
|
|
|
## ...and the second half of the same bug: Global was excluded from the refetch
|
|
## settle entirely, so a canvas born at the wrong size could never heal however
|
|
## the window was resized. Global must take the SIZE refit (it is viewport-sized
|
|
## like every rung now) but never the pan re-float (its canvas is whole-body and
|
|
## origin-anchored — the server ignores `center` for Global), which _refloat_now()
|
|
## would betray by zeroing _view_offset.
|
|
func test_global_takes_the_size_refit_but_never_the_pan_refloat() -> 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)
|
|
|
|
# Drift the view far enough that a fixed rung would hard re-float.
|
|
v._view_offset = Vector2(-100_000.0, -100_000.0)
|
|
v._on_refetch_settle()
|
|
|
|
# _refloat_now() would re-centre the request on whatever world point sits
|
|
# under the viewport centre; Global's canvas is origin-anchored and the
|
|
# server ignores `center` for it, so the world centre must not move.
|
|
# (_view_offset is NOT the probe here — _recompute_canvas_transform()
|
|
# legitimately re-centres it on any canvas adoption.)
|
|
assert_that(v._world_center).override_failure_message(
|
|
"Global has no centre to re-float to — _world_center must stay at the origin"
|
|
).is_equal(Vector2.ZERO)
|
|
|
|
|
|
## 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)
|
|
|
|
|
|
# =============================================================================
|
|
# T-1197 PR #217 review (Hoshe): header-panel-vs-legend-panel vertical
|
|
# non-overlap — the exact regression this review round caught. Mirrors the
|
|
# T-1192 precedent above (test_global_canvas_left_edge_never_overlaps_the_legend_column,
|
|
# line ~674): a geometric non-overlap invariant against the REAL viewer
|
|
# wiring, not a hand-computed expected pixel value that could silently drift
|
|
# out of sync with the production layout the same way the old hardcoded
|
|
# Vector2(PANEL_MARGIN, 60.0) drifted out of sync with the header's real
|
|
# grown footprint.
|
|
# =============================================================================
|
|
|
|
|
|
## The screen header panel and the legend panel must never vertically
|
|
## overlap: the legend's TOP edge (position.y) must be at or below the
|
|
## header's BOTTOM edge (position.y + size.y). Before the T-1197 PR #217 fix,
|
|
## step_canvas_legend.gd's reposition() hardcoded Y=60.0 — a constant tuned
|
|
## for the OLD bare-ImplantHeader footprint — so once the header grew its own
|
|
## ImplantPanel wrapper (border + content margins), the legend's fixed Y sat
|
|
## INSIDE the header panel's new, taller footprint: the two fused into one
|
|
## unbroken double-height box with zero terrain gap between them (pixel-
|
|
## proven independently by both PR #217 reviewers). This test pins the
|
|
## invariant directly against the real _screen_header_panel/_legend_panel
|
|
## Controls the production layout builds, not a copy of the geometry math.
|
|
func test_legend_panel_never_overlaps_the_header_panel_vertically() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
|
|
# Both panels are manually positioned (implant_panel.gd's own doc: "not
|
|
# itself inside a parent Container, so nothing else forces a re-measure"),
|
|
# and reset_to_content_size()/reposition() are deferred — award one idle
|
|
# frame so the REAL settled sizes are in place before asserting, exactly
|
|
# like the deferred-resize idiom both panels already rely on in production
|
|
# (see _build_screen_header()'s own call to reset_to_content_size(), and
|
|
# _ready()'s own deferred reposition() call added alongside this test).
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
|
|
var header_top: float = v._screen_header_panel.position.y
|
|
var header_bottom: float = header_top + v._screen_header_panel.size.y
|
|
var legend_top: float = v._legend_panel.position.y
|
|
|
|
assert_float(legend_top).override_failure_message(
|
|
(
|
|
"the legend panel's top edge (y=%.1f) must be AT OR BELOW the header"
|
|
+ " panel's measured bottom edge (y=%.1f) — a smaller value means the"
|
|
+ " two panels overlap/fuse into one box, the exact PR #217 regression"
|
|
)
|
|
% [legend_top, header_bottom]
|
|
).is_greater_equal(header_bottom - 0.01)
|
|
|
|
|
|
## The gap must be a REAL, visible gap — not just "touching at exactly the
|
|
## same pixel" (which would still satisfy >= but reads as fused on screen).
|
|
## Pins the fixed HEADER_LEGEND_GAP_PX constant is actually being applied,
|
|
## not merely that overlap happens to be avoided by coincidence of content
|
|
## size on this particular test body.
|
|
func test_legend_panel_leaves_a_real_gap_below_the_header_panel() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
|
|
var header_bottom: float = v._screen_header_panel.position.y + v._screen_header_panel.size.y
|
|
var legend_top: float = v._legend_panel.position.y
|
|
var gap: float = legend_top - header_bottom
|
|
|
|
assert_float(gap).override_failure_message(
|
|
(
|
|
"expected a visible gap of at least %.1fpx between the header panel's"
|
|
+ " bottom (y=%.1f) and the legend panel's top (y=%.1f), got %.1fpx —"
|
|
+ " panels that merely touch still read as one fused box on screen"
|
|
)
|
|
% [v.get_header_legend_gap_px(), header_bottom, legend_top, gap]
|
|
).is_greater_equal(v.get_header_legend_gap_px() - 0.01)
|