## T-1153, live round 3 (Jeroen's ruling, design doc §4): tests for ## AtlasWindowTileSet — the orbital rest-state multi-window mosaic ## orchestration. Same hand-built-response-dict conventions as ## test_atlas_window_request.gd/test_atlas_zoom_ladder.gd; this file is ## about the ORCHESTRATION (N tiles, progressive per-tile arrival, ## teardown), not the tile-grid MATH (already covered directly against ## AtlasWindowGeometry.compute_tile_grid() in test_atlas_window_geometry.gd). class_name TestAtlasWindowTileSet extends GdUnitTestSuite const AtlasWindowTileSet := preload("res://ui/implant/apps/atlas/atlas_window_tile_set.gd") const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd") static func _mock_window(center: Vector2i, n: int) -> Dictionary: return { "center": [center.x, center.y], "n": n, "granularity_v2": "Region", "morphology": PackedByteArray([1, 2, 3, 4]), "elev_q": PackedByteArray([10, 20, 30, 40]), "temp_dc": [0, 0, 0, 0], "moisture_q": PackedByteArray([0, 0, 0, 0]), "vegetation": PackedByteArray([0, 0, 0, 0]), "glaciation": PackedByteArray([0, 0, 0, 0]), } static func _mock_response(body_id: String, window: Variant) -> Dictionary: return {"body_id": body_id, "status": "Ready", "district_window": window} ## PR #192 cold-start round 3: the WIRE-ACCURATE shape of a cold body's ## first-ever response — server/src/atlas/layer_proxy.rs's ## get_or_generate()/serve_district_window() on a whole-body cache MISS ## builds `AtlasLayerResponse { status: Pending, district_window: None, ... }` ## (confirmed directly against that source). `body_id` is the ONLY ## identifying field — no center/n/granularity anywhere, matching the real ## wire's total lack of per-request attribution on this specific shape. static func _pending_response(body_id: String) -> Dictionary: return {"body_id": body_id, "status": "Pending", "district_window": null} static func _not_found_response(body_id: String) -> Dictionary: return {"body_id": body_id, "status": "NotFound", "district_window": null} ## Matches atlas_map_protocol.gd's `_decode_status_field()` — the decoded ## `AtlasLayerStatus::Error(String)` variant's status STRING is always just ## "Error" (the message rides in a separate `error` field, not appended to ## the status string), confirmed against that decoder directly. static func _error_response(body_id: String, message: String = "boom") -> Dictionary: return {"body_id": body_id, "status": "Error", "error": message, "district_window": null} func _make_tile_set() -> Variant: var owner_stub := RefCounted.new() var ts = auto_free(AtlasWindowTileSet.new(owner_stub)) add_child(ts) return ts # ============================================================================= # enter() — tile grid computation + one request per tile # ============================================================================= ## enter() on a real, tiling-sized body must produce the SAME tile count ## compute_tile_grid() would — 6 for GJ380c/Lendel, the coordinator's own ## live-round number. func test_enter_produces_the_expected_tile_count_for_lendel() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) assert_int(ts.get_tile_count()).is_equal(6) assert_bool(ts.is_multi_tile()).is_true() ## PR #192 cold-start round 3 hardening: each tile's own AtlasWindowRequest ## must get a DISTINCT, index-matching `_stagger_index` — the anti-storm ## property (deterministic retry-delay stagger, see ## AtlasWindowRequest._retry_delay_for()'s own doc) depends entirely on this ## wiring; without it every tile silently staggers at index 0 and retries ## in lockstep again, exactly the storm risk this hardening exists to close. func test_enter_wires_a_distinct_stagger_index_per_tile() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) for i in range(ts._tiles.size()): var req = ts._tiles[i]["request"] assert_int(req._stagger_index).override_failure_message( "tile %d's request must be wired with _stagger_index=%d, matching" + " its own position in the tile set" % [i, i] ).is_equal(i) ## A tiny (non-tiling) body produces exactly ONE tile — the degenerate case ## compute_tile_grid() itself already covers; this confirms the ORCHESTRATION ## (not just the grid math) handles it without crashing or requesting zero ## tiles. func test_enter_tiny_body_produces_one_tile() -> void: var ts = _make_tile_set() ts.enter("TinyBody", 50.0) assert_int(ts.get_tile_count()).is_equal(1) assert_bool(ts.is_multi_tile()).is_false() ## Every tile must start with a null window (nothing has arrived yet) and ## the tile set must not report "fully arrived" before any response lands. func test_enter_all_tiles_start_unarrived() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) for tile: Dictionary in ts.get_tiles(): assert_that(tile["window"]).is_null() assert_bool(ts.is_fully_arrived()).is_false() ## An empty tile set (never entered) must not report "fully arrived" either ## — an empty AND-over-nothing must not vacuously read true. func test_empty_tile_set_is_not_fully_arrived() -> void: var ts = _make_tile_set() assert_bool(ts.is_fully_arrived()).is_false() # ============================================================================= # Progressive per-tile arrival (design doc §4: "with visible refinement as # tiles complete") — each tile's response is independent of every other's. # ============================================================================= ## Delivering ONE tile's response must populate ONLY that tile's window, ## leaving every other tile still null — the direct "progressive, not ## block-on-all" regression. func test_one_tile_arriving_does_not_affect_the_others() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) var tiles: Array = ts.get_tiles() var first_center: Vector2i = tiles[0]["center"] var window: Dictionary = _mock_window(first_center, AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) var updated_tiles: Array = ts.get_tiles() assert_that(updated_tiles[0]["window"]).override_failure_message( "the tile whose response arrived must have its window populated" ).is_equal(window) for i in range(1, updated_tiles.size()): assert_that(updated_tiles[i]["window"]).override_failure_message( "tile %d must still be unarrived — only tile 0's response was delivered" % i ).is_null() ## tile_ready must fire with the INDEX of the tile that actually arrived — ## the viewer/overlay needs this to know WHICH tile to redraw, not just ## "something changed". func test_tile_ready_signal_fires_with_the_correct_index() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) var received_indices: Array = [] ts.tile_ready.connect(func(index: int) -> void: received_indices.append(index)) var tiles: Array = ts.get_tiles() var second_center: Vector2i = tiles[1]["center"] var window: Dictionary = _mock_window(second_center, AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) assert_int(received_indices.size()).is_equal(1) assert_int(received_indices[0]).is_equal(1) ## Delivering EVERY tile's response must flip is_fully_arrived() to true — ## the mosaic-complete signal the viewer/legend chrome can use. func test_all_tiles_arriving_flips_fully_arrived() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) var tiles: Array = ts.get_tiles() for tile: Dictionary in tiles: var window: Dictionary = _mock_window( tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION ) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) assert_bool(ts.is_fully_arrived()).override_failure_message( "once every tile's response has arrived, the tile set must report fully arrived" ).is_true() # ============================================================================= # PR #192 cold-start round 3 — THE launch-shape regression: a cold body's # FIRST-EVER response is a wire-accurate whole-response Pending (status # "Pending", district_window null, body_id only — no center/n/granularity # anywhere, confirmed against server/src/atlas/layer_proxy.rs directly). # Driven through the REAL fan-out (SimBridge.atlas_layers_received.emit(), # reaching every tile via tile_set._on_atlas_layers_received), not a direct # tile.on_response() call — the coordinator's own regression-discipline ask, # since a direct per-tile call is implicit attribution a real wire fan-out # doesn't have. Before the status-gate fix, on_response()'s FIRST line # (`status != "Ready" -> return`) dropped this response for every tile # before ever reaching the retry-scheduling code — retries stayed at 0 # forever, matching the coordinator's own live cold-server capture exactly. # ============================================================================= ## The exact bug: a whole-response Pending on cold entry must increment ## every tile's retry counter and schedule a re-poll — not be silently ## dropped. Fails hard against the pre-fix code (retries stay 0 forever). func test_cold_entry_whole_response_pending_increments_every_tiles_retry_count() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) SimBridge.atlas_layers_received.emit(_pending_response("GJ380c")) for tile_dict: Dictionary in ts._tiles: var req = tile_dict["request"] assert_bool(req.is_pending()).override_failure_message( "every tile must still be pending after a whole-response Pending" ).is_true() assert_int(req._retries).override_failure_message( "a whole-response Pending must increment the retry counter — the" + " exact bug: the OLD status-gate silently dropped this before" + " ever reaching the retry-scheduling code, leaving retries at 0 forever" ).is_equal(1) ## The full convergence: repeated whole-response Pendings (simulating a slow ## cold AnalyzeBody), THEN real per-tile Ready responses for the tiles' own ## RE-REQUESTS — every tile must eventually fill. No re-request means this ## hangs (waiting past the retry delay for a re-poll that never happens) or ## fails (has_pending_tiles() never flips false) — exactly the launch-shape ## gap the coordinator named: "every unit test delivered Ready immediately." func test_cold_entry_converges_after_repeated_pending_then_real_readies() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) var tiles: Array = ts.get_tiles() # Two consecutive whole-response Pendings — a slow cold derive, not a # single flip. Real waits so the scheduled retry timers can actually fire. SimBridge.atlas_layers_received.emit(_pending_response("GJ380c")) await get_tree().create_timer(0.7).timeout # past the first backoff delay SimBridge.atlas_layers_received.emit(_pending_response("GJ380c")) await get_tree().create_timer(0.8).timeout # past the second (staggered) delay assert_bool(ts.has_pending_tiles()).override_failure_message( "sanity: still pending after 2 cold cycles" ).is_true() for tile_dict: Dictionary in ts._tiles: var req = tile_dict["request"] assert_int(req._retries).override_failure_message( "sanity: each tile's retry counter must have actually incremented" + " twice — otherwise the final assertion below would pass even if" + " the retries never happened at all (a fresh Ready was never the" + " broken behavior, only the retry itself was)" ).is_equal(2) for tile: Dictionary in tiles: var window: Dictionary = _mock_window( tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION ) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) assert_bool(ts.has_pending_tiles()).override_failure_message( "after the retries fire and real Readies land, every tile must have adopted its window" ).is_false() assert_bool(ts.is_fully_arrived()).is_true() ## NotFound must give up IMMEDIATELY, not retry — a body that doesn't exist ## will never resolve by waiting (the coordinator's own "give up on error ## responses, not elapsed patience" framing — this replaces the old ## elapsed-retries-only give-up policy with a correctness-based one for the ## cases where retrying is provably pointless). func test_cold_entry_not_found_gives_up_immediately_without_retry() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) SimBridge.atlas_layers_received.emit(_not_found_response("GJ380c")) for tile_dict: Dictionary in ts._tiles: var req = tile_dict["request"] assert_bool(req.is_pending()).override_failure_message( "NotFound must give up immediately, not stay pending waiting for a retry" ).is_false() assert_int(req._retries).override_failure_message( "NotFound must never increment the retry counter — retrying a" + " nonexistent body is provably pointless" ).is_equal(0) ## Error must give up immediately too, same reasoning as NotFound — a ## resolve/IO failure won't resolve itself by polling. func test_cold_entry_error_gives_up_immediately_without_retry() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) SimBridge.atlas_layers_received.emit(_error_response("GJ380c")) for tile_dict: Dictionary in ts._tiles: var req = tile_dict["request"] assert_bool(req.is_pending()).override_failure_message( "Error must give up immediately, not stay pending waiting for a retry" ).is_false() assert_int(req._retries).is_equal(0) # ============================================================================= # has_pending_tiles() / has_any_tile_arrived() — PR #192 cold-start dossier. # Distinct predicates (both can be true at once, mid-arrival): the viewer's # self-healing redraw (BUG 1) polls has_pending_tiles(); the "DERIVING # TERRAIN…" label (BUG 3) polls has_any_tile_arrived() to know when to drop. # ============================================================================= func test_has_pending_tiles_true_immediately_after_enter() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) assert_bool(ts.has_pending_tiles()).override_failure_message( "every tile is unarrived right after enter() — has_pending_tiles() must be true" ).is_true() func test_has_pending_tiles_false_once_every_tile_has_arrived() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) for tile: Dictionary in ts.get_tiles(): var window: Dictionary = _mock_window( tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION ) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) assert_bool(ts.has_pending_tiles()).is_false() func test_has_pending_tiles_true_while_only_some_tiles_have_arrived() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) var first_tile: Dictionary = ts.get_tiles()[0] var window: Dictionary = _mock_window( first_tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION ) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) assert_bool(ts.has_pending_tiles()).override_failure_message( "5 of 6 tiles still unarrived — has_pending_tiles() must stay true" ).is_true() func test_has_any_tile_arrived_false_immediately_after_enter() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) assert_bool(ts.has_any_tile_arrived()).override_failure_message( "nothing has arrived right after enter() — has_any_tile_arrived() must be false" ).is_false() ## The exact mid-arrival case both predicates must agree can coexist: one ## tile in, five still pending — the point the "DERIVING TERRAIN…" label ## must drop (has_any_tile_arrived() flips true) while the self-heal must ## keep redrawing (has_pending_tiles() stays true). func test_has_any_tile_arrived_true_after_a_single_tile_lands() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) var first_tile: Dictionary = ts.get_tiles()[0] var window: Dictionary = _mock_window( first_tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION ) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window)) assert_bool(ts.has_any_tile_arrived()).is_true() assert_bool(ts.has_pending_tiles()).override_failure_message( "sanity: the other 5 tiles are still pending at the same moment" ).is_true() func test_has_any_tile_arrived_false_for_an_empty_tile_set() -> void: var ts = _make_tile_set() assert_bool(ts.has_any_tile_arrived()).override_failure_message( "an empty tile set (never entered) must not vacuously report arrival" ).is_false() ## A response for a body the tile set is NOT currently showing (a stale ## response from a body the player has since navigated away from) must not ## be adopted by any tile — the SAME body_id staleness guard every other ## AtlasWindowRequest-based path already relies on (this is inherited for ## free since each tile IS an AtlasWindowRequest, but pinned here as an ## orchestration-level regression too). func test_response_for_a_different_body_is_ignored() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) var tiles: Array = ts.get_tiles() var window: Dictionary = _mock_window( tiles[0]["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION ) SimBridge.atlas_layers_received.emit(_mock_response("GJ_wrong_body", window)) assert_that(ts.get_tiles()[0]["window"]).is_null() # ============================================================================= # Teardown — re-entering (a fresh body, or the same body again) must not # leave stale tile request nodes wired up. # ============================================================================= ## Calling enter() a SECOND time (e.g. re-entering the orbital frame, or ## switching to a different body) must replace the tile set entirely — the ## OLD tiles' indices/centers must not linger. func test_second_enter_replaces_the_tile_set() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) var first_count: int = ts.get_tile_count() assert_int(first_count).is_equal(6) ts.enter("TinyBody", 50.0) assert_int(ts.get_tile_count()).override_failure_message( "a second enter() must fully replace the tile set, not append to it" ).is_equal(1) ## A response matching an OLD tile set's (body, center) — arriving AFTER a ## second enter() has already torn it down — must not be adopted (or crash): ## the old tile's AtlasWindowRequest node is queue_free()'d, and _tiles no ## longer references it, so a stale signal (if it could somehow still fire) ## has no live entry left to update. func test_stale_response_after_second_enter_does_not_crash_or_leak() -> void: var ts = _make_tile_set() ts.enter("GJ380c", 6238.4) var old_tiles: Array = ts.get_tiles() var old_center: Vector2i = old_tiles[0]["center"] ts.enter("GJ380c", 50.0) # same body_id, different (tiny) radius -> different tile grid # A response shaped like it's answering the OLD tile set's first tile — # must not crash, and must not corrupt the NEW tile set's single tile. var stale_window: Dictionary = _mock_window( old_center, AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION ) SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", stale_window)) assert_int(ts.get_tile_count()).is_equal(1)