Entry per Jeroen's 2026-07-21 revision: planetary heightmap is now FIXED — all drag-pan/wheel-zoom input removed (set_view/get_view_* capture API survives for the golden harness); hover shows a not-to-scale bracket reticle with the real extent labeled (a true n=32 rectangle is sub-pixel on the planetary canvas — the honest representation given the morph transition is deferred), and a click that misses every city marker descends (city-click wins — one gesture, two contextual reads, no modifier). Descent pushes a new 'district' nav screen centered on the click point's DistrictPos via atlas_descend_geometry.district_pos_at (the pixel-to-district inverse of the server mapping, verified against scale.rs). Regional mode: atlas_window_viewer draws the composite (morphology x elev_q lightness base; temp/moisture/veg toggles — temp reuses the region-ramp colorizer exactly; Marine=6 transparent; glaciation always-on tint matching apply_ice_tint's REAL gate, None|Light no-op, over the amendment's looser prose — documented); pan-on-held-composite with edge-crossing refetch + border-fade during the queue-based derive wait; zoom never refetches. atlas_window_cache: LRU keyed (body_id, center, n), touch-on-read, evict-only, no freshness (D-227). atlas_window_request mirrors the generation-proxy pending-retry shape for None-until-derived. Codec: window params omitted from the wire when absent — byte-identical for every existing caller. Live-verified against the T-1137 server in-worktree: real round-trip on a GJ380c coastal district (6 fields x 1024 cells), echo staleness guard, genuine ~1.4s background-derive wait, pan-edge refetch to an adjacent window, cache-hit on re-descent with zero network. Full client suite 3194/3194; gdlint clean on all 18 files. Open follow-ups flagged in-code: header location label always falls back to coordinates (nearest-settlement needs a join the district window does not carry); atlas_standalone.gd's 'atlas_app.gd is never modified' doc line is now imprecise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
205 lines
8.4 KiB
GDScript
205 lines
8.4 KiB
GDScript
## T-1138 (D-226 T-1124 amendment §1-§5): tests for AtlasWindowViewer + its
|
|
## companion request/cache orchestration (atlas_window_request.gd) — pure
|
|
## logic against hand-built AtlasLayerResponse-shaped dicts, matching the
|
|
## ticket's "unit tests against hand-built response dicts" instruction. Live
|
|
## end-to-end verification against a real spawned server is separate
|
|
## (companion-run evidence, not gdUnit — this file never touches SimBridge's
|
|
## live-mode path, only the response-handling/cache/overlay logic that path
|
|
## eventually feeds).
|
|
class_name TestAtlasWindowViewer
|
|
extends GdUnitTestSuite
|
|
|
|
# atlas_window_request.gd has no class_name (review #8 precedent throughout
|
|
# this cluster) — preloaded once here, not re-load()ed per test (gdlint
|
|
# duplicated-load).
|
|
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
|
|
|
|
|
|
## Build a hand-authored DistrictWindowLayer dict (n=2, matching the shape
|
|
## district_grid/region_grid fixtures already use elsewhere in this suite).
|
|
static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary:
|
|
return {
|
|
"center": [center.x, center.y],
|
|
"n": n,
|
|
"morphology": PackedByteArray([8, 14, 0, 1]),
|
|
"elev_q": PackedByteArray([40, 90, 5, 60]),
|
|
"temp_dc": [120, 95, -32768, 60],
|
|
"moisture_q": PackedByteArray([50, 30, 90, 20]),
|
|
"vegetation": PackedByteArray([2, 1, 6, 3]),
|
|
"glaciation": PackedByteArray([0, 0, 1, 2]),
|
|
}
|
|
|
|
|
|
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
|
|
return {"body_id": body_id, "status": "Ready", "district_window": window}
|
|
|
|
|
|
# =============================================================================
|
|
# AtlasWindowViewer — entry + overlay defs
|
|
# =============================================================================
|
|
|
|
|
|
func test_enter_with_no_response_leaves_window_null_and_pending() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20))
|
|
assert_that(v.get_district_window()).is_null()
|
|
|
|
|
|
## Feeding a matching Ready response (via the SAME SimBridge.atlas_layers_received
|
|
## routing path the viewer subscribes to in _ready()) must populate the window.
|
|
func test_enter_then_matching_response_populates_window() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
|
|
|
|
var window: Dictionary = _mock_window(Vector2i(10, 20), 2)
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
|
|
|
|
assert_that(v.get_district_window()).is_equal(window)
|
|
|
|
|
|
## A response for a DIFFERENT body must not populate the window — the
|
|
## body_id scoping AtlasWindowRequest.on_response() checks.
|
|
func test_response_for_different_body_is_ignored() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
|
|
|
|
var window: Dictionary = _mock_window(Vector2i(10, 20), 2)
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ_wrong_body", window))
|
|
|
|
assert_that(v.get_district_window()).is_null()
|
|
|
|
|
|
## A response whose echoed (center, n) does NOT match what was last asked for
|
|
## is stale — §2's race-condition guard. Simulates a superseded-by-a-later-pan
|
|
## response arriving after the fact.
|
|
func test_response_with_mismatched_echo_is_discarded_as_stale() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
|
|
|
|
var stale_window: Dictionary = _mock_window(Vector2i(99, 99), 2) # wrong center
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", stale_window))
|
|
|
|
assert_that(v.get_district_window()).is_null()
|
|
|
|
|
|
## §1: an as-yet-underived window rides as `district_window: None` inside a
|
|
## Ready response — this is NOT an error, the viewer just keeps waiting
|
|
## (get_district_window() stays null, no crash, no window content shown).
|
|
func test_ready_response_with_null_district_window_keeps_waiting() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", null))
|
|
assert_that(v.get_district_window()).is_null()
|
|
|
|
|
|
func test_overlay_defs_include_the_three_toggle_ids() -> void:
|
|
var ids: Array = []
|
|
for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS:
|
|
ids.append(d["id"])
|
|
assert_that(ids).contains(["gen_dw_temp", "gen_dw_moisture", "gen_dw_veg"])
|
|
|
|
|
|
## Glaciation is explicitly NOT a toggle id (§5: "an always-on modifier, not
|
|
## a toggle") — a regression here would silently re-introduce it as a switch.
|
|
func test_overlay_defs_do_not_include_glaciation() -> void:
|
|
var ids: Array = []
|
|
for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS:
|
|
ids.append(d["id"])
|
|
assert_that(ids).not_contains(["gen_dw_glaciation", "gen_dw_ice"])
|
|
|
|
|
|
func test_set_overlay_visible_toggles_and_is_overlay_visible_reflects_it() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_false()
|
|
v.set_overlay_visible("gen_dw_temp", true)
|
|
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_true()
|
|
|
|
|
|
func test_set_overlay_visible_unknown_id_is_a_noop() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.set_overlay_visible("not_a_real_overlay", true)
|
|
assert_bool(v.is_overlay_visible("not_a_real_overlay")).is_false()
|
|
|
|
|
|
# =============================================================================
|
|
# T-1120 capture-API parity (the ticket's explicit note: must survive here too)
|
|
# =============================================================================
|
|
|
|
|
|
func test_set_view_and_getters_round_trip() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.set_view(2.5, Vector2(30.0, -10.0))
|
|
assert_that(v.get_view_zoom()).is_equal_approx(2.5, 0.001)
|
|
assert_that(v.get_view_offset()).is_equal(Vector2(30.0, -10.0))
|
|
|
|
|
|
func test_set_view_clamps_to_min_max_zoom() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.set_view(0.01, Vector2.ZERO)
|
|
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.001)
|
|
v.set_view(1000.0, Vector2.ZERO)
|
|
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MAX_ZOOM, 0.001)
|
|
|
|
|
|
# =============================================================================
|
|
# AtlasWindowRequest — cache reuse (§4's Esc-then-re-enter / pan-back hit)
|
|
# =============================================================================
|
|
|
|
|
|
func test_window_request_cache_hit_emits_synchronously_no_pending() -> void:
|
|
var owner_stub := RefCounted.new()
|
|
var req = auto_free(AtlasWindowRequest.new(owner_stub))
|
|
add_child(req)
|
|
|
|
# Prime the cache directly (bypassing the network path) — the ticket's
|
|
# own instruction: unit test against hand-built response dicts.
|
|
req.get_cache().put("GJ380c", Vector2i(1, 1), 2, _mock_window(Vector2i(1, 1), 2))
|
|
|
|
var received: Array = []
|
|
req.window_ready.connect(func(w: Dictionary) -> void: received.append(w))
|
|
req.request_now("GJ380c", Vector2i(1, 1), 2)
|
|
|
|
assert_int(received.size()).is_equal(1)
|
|
assert_bool(req.is_pending()).override_failure_message(
|
|
"a cache hit must never leave the request pending"
|
|
).is_false()
|
|
|
|
|
|
func test_window_request_cache_miss_leaves_pending_true() -> void:
|
|
var owner_stub := RefCounted.new()
|
|
var req = auto_free(AtlasWindowRequest.new(owner_stub))
|
|
add_child(req)
|
|
req.request_now("GJ380c", Vector2i(5, 5), 2)
|
|
assert_bool(req.is_pending()).is_true()
|
|
|
|
|
|
## on_response() with a matching Ready+window response resolves the pending
|
|
## request AND populates the cache — verified by a second request_now() call
|
|
## for the same (center, n) becoming a cache hit with zero additional pending.
|
|
func test_on_response_resolves_and_populates_cache_for_next_request() -> void:
|
|
var owner_stub := RefCounted.new()
|
|
var req = auto_free(AtlasWindowRequest.new(owner_stub))
|
|
add_child(req)
|
|
|
|
req.request_now("GJ380c", Vector2i(2, 2), 2)
|
|
assert_bool(req.is_pending()).is_true()
|
|
|
|
var window: Dictionary = _mock_window(Vector2i(2, 2), 2)
|
|
req.on_response(_mock_response("GJ380c", window))
|
|
assert_bool(req.is_pending()).is_false()
|
|
|
|
# Re-request the SAME (body, center, n) — must be a cache hit, no pending.
|
|
req.request_now("GJ380c", Vector2i(2, 2), 2)
|
|
assert_bool(req.is_pending()).override_failure_message(
|
|
"a second request for an already-resolved window must hit the cache"
|
|
).is_false()
|