diff --git a/client/tests/test_atlas_zoom_ladder.gd b/client/tests/test_atlas_zoom_ladder.gd index d510893c7..544fc27be 100644 --- a/client/tests/test_atlas_zoom_ladder.gd +++ b/client/tests/test_atlas_zoom_ladder.gd @@ -59,16 +59,89 @@ func test_enter_orbital_requests_region_granularity() -> void: assert_str(v._held_granularity_v2).is_equal("Region") -## enter_orbital()'s `n` must equal the body's full equatorial circumference +## enter_orbital()'s `n` intent is the body's full equatorial circumference ## in districts (district_extent().cols) — the whole body fitted to the -## canvas, per Jeroen's HARD condition wording. -func test_enter_orbital_n_covers_the_full_circumference() -> void: +## canvas, per Jeroen's HARD condition wording — BUT `_held_n` is what +## actually gets STORED/SENT, and that must be the CLAMPED value +## (live-round finding: the raw cols value, routinely tens of thousands at +## Region granularity, was stored unclamped while AtlasWindowRequest clamped +## before sending — see _enter_at_rung()'s own doc for the full C1-one-layer-up +## story). GJ380c's real cols (~19,139 per the live repro) exceeds +## DISTRICT_WINDOW_MAX_N_REGION's clamp ceiling, so this body is the exact +## regression case, not a hypothetical. +func test_enter_orbital_n_is_the_clamped_value_not_raw_circumference() -> void: var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) add_child(v) - var radius_km := 6238.4 + var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km) + var raw_cols: int = int(extent["cols"]) v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {}) - assert_int(v._held_n).is_equal(int(extent["cols"])) + + var expected_clamped: int = AtlasWindowRequest._clamp_window_n_mirror_v2(raw_cols, "Region") + var failure_msg: String = ( + "_held_n must be the CLAMPED n (%d), matching what the server will echo —" + + " not the raw circumference (%d), which the server would clamp down and" + + " every response would then fail the w_n != _held_n staleness check" + ) % [expected_clamped, raw_cols] + assert_int(v._held_n).override_failure_message(failure_msg).is_equal(expected_clamped) + # GJ380c's raw circumference must actually exceed the clamp — otherwise this + # test would pass trivially without exercising the clamp at all. + assert_int(raw_cols).override_failure_message( + "GJ380c's raw district-column count must exceed the Region clamp ceiling" + + " for this to be a real regression guard, not a no-op" + ).is_greater(expected_clamped) + + +## **The live-round regression, end to end:** enter_orbital() on a +## real-sized body (GJ380c/Lendel, radius 6238.4 km, raw cols far past the +## Region clamp ceiling) followed by a server response echoing the CLAMPED +## n + "Region" granularity must be ACCEPTED and become the held window — not +## silently dropped as stale forever (the exact live bug: `wv._held_n = +## 19139` vs. echoed `6400`, blank ladder on every real-sized body). This is +## the round-trip the existing suite never exercised — every prior +## enter_orbital() test asserted on request-side state only, never delivered +## a response. +func test_enter_orbital_oversized_body_accepts_the_clamped_region_response() -> void: + var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) + add_child(v) + var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body + var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km) + var raw_cols: int = int(extent["cols"]) + var clamped_n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(raw_cols, "Region") + # Sanity: this body must actually need clamping, or the test proves nothing. + assert_int(raw_cols).is_greater(clamped_n) + + v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {}) + assert_that(v.get_district_window()).override_failure_message( + "no response delivered yet — must still be null" + ).is_null() + + # The server's real response: echoes the CLAMPED n, "Region" granularity, + # center (0,0) — exactly what handle_atlas_request/clamp_window_n_v2 + # actually produces for an oversized orbital request. + var region_window: Dictionary = { + "center": [0, 0], + "n": clamped_n, + "granularity_v2": "Region", + "morphology": PackedByteArray([8, 14, 0, 1]), + "elev_q": PackedByteArray([40, 90, 5, 60]), + "temp_dc": [120, 95, -32768, 60], + "moisture_q": PackedByteArray([50, 30, 90, 20]), + "vegetation": PackedByteArray([2, 1, 6, 3]), + "glaciation": PackedByteArray([0, 0, 1, 2]), + } + SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", region_window)) + + var failure_msg: String = ( + "a response echoing the server's own clamped n + Region granularity must be" + + " ACCEPTED and become the held window — the live bug left this permanently" + + " null (w_n=%d never matched a stale unclamped _held_n=%d) on every" + + " real-sized body" + ) % [clamped_n, raw_cols] + assert_that(v.get_district_window()).override_failure_message(failure_msg).is_equal( + region_window + ) + assert_str(v._held_granularity_v2).is_equal("Region") ## A no-radius body (tiny test body) has no circumference concept — diff --git a/client/ui/implant/apps/atlas/atlas_window_viewer.gd b/client/ui/implant/apps/atlas/atlas_window_viewer.gd index cc53bfe7b..1325ae086 100644 --- a/client/ui/implant/apps/atlas/atlas_window_viewer.gd +++ b/client/ui/implant/apps/atlas/atlas_window_viewer.gd @@ -330,6 +330,21 @@ func enter_orbital(body: Dictionary, system: Dictionary) -> void: ## no canonicalization; enter()'s does its own before calling in). Resets ## every piece of held/request state for a fresh descent, exactly as the ## pre-T-1153 enter() always did, plus the new _held_granularity_v2 tracking. +## +## **The C1 clamp-mirror lesson, one layer up (live-round finding):** `n` +## MUST be clamped via `_clamp_window_n_mirror_v2()` BEFORE it becomes +## `_held_n` — mirroring exactly what AtlasWindowRequest.request_now() +## already does to ITS OWN `_n` before storing/sending (see that function's +## own doc for the original PR #191 Tyre C1 finding). Storing the RAW `n` +## here (e.g. enter_orbital()'s full district_extent().cols, routinely tens +## of thousands at Region granularity, versus the server's clamped echo of +## at most DISTRICT_WINDOW_MAX_N_REGION=6,400) left `_held_n` permanently +## disagreeing with what the server would ever actually echo — every +## orbital-rung response was silently rejected as stale by +## _on_window_ready()'s `w_n != _held_n` check, hanging the ladder on every +## real-sized body. `_maybe_reselect_rung()`/`_maybe_refloat_window()` both +## read `_held_n` (never re-derive it), so clamping here — the ONE write +## site — fixes every downstream caller too, not just entry. func _enter_at_rung( body: Dictionary, system: Dictionary, @@ -337,17 +352,20 @@ func _enter_at_rung( n: int, granularity_v2: String ) -> void: + var clamped_n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(n, granularity_v2) _body = body _system = system _held_center = district_center - _held_n = n + _held_n = clamped_n _held_granularity_v2 = granularity_v2 _window = null _user_adjusted = false _awaiting_first_window = true _fit_and_center() _window_request.reset() - _window_request.request_now(_dict_str(_body, "body_id", ""), _held_center, n, granularity_v2) + _window_request.request_now( + _dict_str(_body, "body_id", ""), _held_center, clamped_n, granularity_v2 + ) _refresh_screen_header() grab_focus() queue_redraw()