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>
122 lines
5.3 KiB
GDScript
122 lines
5.3 KiB
GDScript
## T-1138 (D-226 T-1124 amendment §4): tests for the client-side
|
|
## DistrictWindowLayer LRU cache — keyed on (body_id, center, n), LRU-evict
|
|
## only (D-227 determinism means no freshness check is ever needed).
|
|
class_name TestAtlasWindowCache
|
|
extends GdUnitTestSuite
|
|
|
|
const AtlasWindowCache := preload("res://ui/implant/apps/atlas/atlas_window_cache.gd")
|
|
|
|
|
|
func test_make_key_distinguishes_body_center_and_n() -> void:
|
|
var k1 := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32)
|
|
var k2 := AtlasWindowCache.make_key("GJ1d", Vector2i(10, 20), 32) # different body
|
|
var k3 := AtlasWindowCache.make_key("GJ1c", Vector2i(11, 20), 32) # different center
|
|
var k4 := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 64) # different n
|
|
assert_str(k1).is_not_equal(k2)
|
|
assert_str(k1).is_not_equal(k3)
|
|
assert_str(k1).is_not_equal(k4)
|
|
|
|
|
|
func test_miss_returns_null_and_has_reports_false() -> void:
|
|
var cache := AtlasWindowCache.new()
|
|
assert_that(cache.get_window("GJ1c", Vector2i(0, 0), 32)).is_null()
|
|
assert_bool(cache.has("GJ1c", Vector2i(0, 0), 32)).is_false()
|
|
|
|
|
|
func test_put_then_get_round_trips_exact_window() -> void:
|
|
var cache := AtlasWindowCache.new()
|
|
var window := {"center": [10, 20], "n": 32, "morphology": PackedByteArray([1, 2, 3])}
|
|
cache.put("GJ1c", Vector2i(10, 20), 32, window)
|
|
assert_bool(cache.has("GJ1c", Vector2i(10, 20), 32)).is_true()
|
|
assert_that(cache.get_window("GJ1c", Vector2i(10, 20), 32)).is_equal(window)
|
|
|
|
|
|
## D-227: a window fetched once is valid FOREVER for that (body, center, n) —
|
|
## no expiry, no invalidation path. Repeated get_window() calls across a
|
|
## simulated time gap (just repeated calls here — there is no clock in this
|
|
## cache at all) must keep returning the same stored value.
|
|
func test_cached_window_never_expires() -> void:
|
|
var cache := AtlasWindowCache.new()
|
|
var window := {"center": [0, 0], "n": 32}
|
|
cache.put("GJ1c", Vector2i(0, 0), 32, window)
|
|
for _i in range(50):
|
|
assert_that(cache.get_window("GJ1c", Vector2i(0, 0), 32)).is_equal(window)
|
|
|
|
|
|
## Different (body_id, center, n) tuples never collide — this is the whole
|
|
## point of the composite key (§4: "cacheable client-side keyed on
|
|
## (body_id, center, n)").
|
|
func test_different_bodies_same_center_do_not_collide() -> void:
|
|
var cache := AtlasWindowCache.new()
|
|
var window_a := {"body": "GJ1c"}
|
|
var window_b := {"body": "GJ1d"}
|
|
cache.put("GJ1c", Vector2i(10, 20), 32, window_a)
|
|
cache.put("GJ1d", Vector2i(10, 20), 32, window_b)
|
|
assert_that(cache.get_window("GJ1c", Vector2i(10, 20), 32)).is_equal(window_a)
|
|
assert_that(cache.get_window("GJ1d", Vector2i(10, 20), 32)).is_equal(window_b)
|
|
|
|
|
|
## Overwriting the same key replaces the value (e.g. a re-fetch of an
|
|
## already-cached window from a server that somehow returns a different
|
|
## payload — should never happen under D-227, but the cache itself must not
|
|
## silently keep the stale one on an explicit put()).
|
|
func test_put_overwrites_existing_key() -> void:
|
|
var cache := AtlasWindowCache.new()
|
|
cache.put("GJ1c", Vector2i(0, 0), 32, {"v": 1})
|
|
cache.put("GJ1c", Vector2i(0, 0), 32, {"v": 2})
|
|
assert_int(cache.size()).is_equal(1)
|
|
assert_that(cache.get_window("GJ1c", Vector2i(0, 0), 32)).is_equal({"v": 2})
|
|
|
|
|
|
# =============================================================================
|
|
# LRU eviction
|
|
# =============================================================================
|
|
|
|
|
|
func test_eviction_drops_least_recently_used_on_overflow() -> void:
|
|
var cache := AtlasWindowCache.new(2) # max 2 entries
|
|
cache.put("GJ1c", Vector2i(0, 0), 32, {"id": "a"})
|
|
cache.put("GJ1c", Vector2i(1, 0), 32, {"id": "b"})
|
|
cache.put("GJ1c", Vector2i(2, 0), 32, {"id": "c"}) # evicts (0,0) — oldest, untouched
|
|
|
|
assert_int(cache.size()).is_equal(2)
|
|
assert_bool(cache.has("GJ1c", Vector2i(0, 0), 32)).override_failure_message(
|
|
"oldest entry should have been evicted"
|
|
).is_false()
|
|
assert_bool(cache.has("GJ1c", Vector2i(1, 0), 32)).is_true()
|
|
assert_bool(cache.has("GJ1c", Vector2i(2, 0), 32)).is_true()
|
|
|
|
|
|
## get_window() touches an entry (moves it to most-recently-used) — reading an
|
|
## entry must protect it from the NEXT eviction, otherwise "LRU" degrades to
|
|
## FIFO the moment anything reads from the cache (the common case: Esc-then-
|
|
## re-enter and pan-back are exactly "read a recently-cached window", §4).
|
|
func test_get_touches_entry_and_protects_it_from_eviction() -> void:
|
|
var cache := AtlasWindowCache.new(2)
|
|
cache.put("GJ1c", Vector2i(0, 0), 32, {"id": "a"})
|
|
cache.put("GJ1c", Vector2i(1, 0), 32, {"id": "b"})
|
|
cache.get_window("GJ1c", Vector2i(0, 0), 32) # touch (0,0) — now most-recently-used
|
|
cache.put("GJ1c", Vector2i(2, 0), 32, {"id": "c"}) # should evict (1,0), not (0,0)
|
|
|
|
assert_bool(cache.has("GJ1c", Vector2i(0, 0), 32)).override_failure_message(
|
|
"touched entry should survive eviction"
|
|
).is_true()
|
|
assert_bool(cache.has("GJ1c", Vector2i(1, 0), 32)).override_failure_message(
|
|
"untouched entry should be the one evicted"
|
|
).is_false()
|
|
|
|
|
|
func test_max_entries_clamped_to_at_least_one() -> void:
|
|
var cache := AtlasWindowCache.new(0)
|
|
cache.put("GJ1c", Vector2i(0, 0), 32, {"id": "a"})
|
|
cache.put("GJ1c", Vector2i(1, 0), 32, {"id": "b"})
|
|
assert_int(cache.size()).is_equal(1)
|
|
|
|
|
|
func test_clear_empties_the_cache() -> void:
|
|
var cache := AtlasWindowCache.new()
|
|
cache.put("GJ1c", Vector2i(0, 0), 32, {"id": "a"})
|
|
cache.clear()
|
|
assert_int(cache.size()).is_equal(0)
|
|
assert_bool(cache.has("GJ1c", Vector2i(0, 0), 32)).is_false()
|