The atlas 'regional' screen now opens the LADDER at the canonical orbital frame (Region granularity, whole body fitted and centered) and wheel zoom descends continuously — cursor-anchored, unclamped across rungs, with progressive refinement (held composite keeps drawing, finer rung swaps in place on arrival; no blank frame, no mode flip). Full-zoom-out resets to the canonical planetary frame per Jeroen's HARD condition (is_fully_zoomed_out = extent >= body circumference, not a zoom-value heuristic). The district_screen nav hop is deleted — D-013 restored: descent is a zoom gesture, not a nav push. AtlasViewer's heightmap-texture path is unreachable from nav (code intact; overlay surface deferred, see report/tickets). Rung selection: design doc §5's literal formula has NO legal District band at any real viewport (visual-tolerance band and n=64 coverage ceiling never overlap — pinned by executable boundary tests at 1600x900); select_rung() splits it into a coverage ceiling (decides Region) then the 2x visual tolerance (District vs Quarter), documented at the function. In practice the ladder steps Region -> Quarter directly. Wire: window_granularity_v2 encoded (omitted at District for byte-compat), granularity_v2 echoed value keyed + staleness-guarded end to end; Region clamp mirror replicates the server's bounded halving loop (no closed form). MIN/MAX_ZOOM widened to [0.0005, 64] — the old 0.5 floor would have clamped a real body's canonical fit zoom, violating the reset condition. Real pre-existing bug fixed in atlas_window_overlay.gd: the draw path used echoed n as both cell-grid dimension and district extent — only coincidentally correct at District granularity; Quarter/Region would have read wrong array offsets. cell_grid_side_for_window() now mirrors the server's WindowGranularity::cell_grid_side. Tests: +26 pure-function geometry tests, new 30-test zoom-ladder suite, extensions across the window cache/request/overlay/delivery suites. Full suite 3518 green; cold-parse clean.
252 lines
11 KiB
GDScript
252 lines
11 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()
|
|
|
|
|
|
# =============================================================================
|
|
# granularity / min_wl_m (T-1150, zoom ladder design doc §3 aliasing risk)
|
|
# =============================================================================
|
|
|
|
|
|
## **MANDATORY aliasing regression (client half, T-1150):** a granularity-4
|
|
## (quarter) key and a granularity-1 (district) key at the IDENTICAL
|
|
## (body_id, center, n) must be DISTINCT cache keys — this is what prevents a
|
|
## quarter-spacing request from silently reading (or overwriting) a
|
|
## district-spacing window's cache entry, and vice versa.
|
|
func test_make_key_distinguishes_granularity_at_identical_body_center_n() -> void:
|
|
var k_district := AtlasWindowCache.make_key(
|
|
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY
|
|
)
|
|
var k_quarter := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32, 4)
|
|
assert_str(k_district).is_not_equal(k_quarter)
|
|
|
|
|
|
## Same aliasing risk, the other new axis: two requests identical except for
|
|
## `min_wl_m` (the octave cutoff) must not collide either — different cutoffs
|
|
## are different derived payloads (T-1149/T-1150).
|
|
func test_make_key_distinguishes_min_wl_m_at_identical_body_center_n_granularity() -> void:
|
|
var k_uncut := AtlasWindowCache.make_key(
|
|
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0
|
|
)
|
|
var k_cut := AtlasWindowCache.make_key(
|
|
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 512
|
|
)
|
|
assert_str(k_uncut).is_not_equal(k_cut)
|
|
|
|
|
|
## Omitting granularity/min_wl_m (every pre-T-1150 call site) must produce the
|
|
## SAME key as passing the explicit district/no-cutoff defaults — byte/string
|
|
## compatibility for existing callers, not just "doesn't crash".
|
|
func test_omitted_granularity_and_min_wl_m_match_explicit_district_defaults() -> void:
|
|
var k_omitted := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32)
|
|
var k_explicit := AtlasWindowCache.make_key(
|
|
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0
|
|
)
|
|
assert_str(k_omitted).is_equal(k_explicit)
|
|
|
|
|
|
## End-to-end through put()/get_window()/has() (not just make_key() in
|
|
## isolation): a quarter-granularity window and a district-granularity window
|
|
## at the identical (body, center, n) must both be independently retrievable,
|
|
## neither one clobbering or masking the other.
|
|
func test_district_and_quarter_windows_coexist_at_identical_body_center_n() -> void:
|
|
var cache := AtlasWindowCache.new()
|
|
var district_window := {"granularity": AtlasWindowCache.DISTRICT_GRANULARITY, "id": "district"}
|
|
var quarter_window := {"granularity": 4, "id": "quarter"}
|
|
|
|
cache.put("GJ1c", Vector2i(10, 20), 32, district_window, AtlasWindowCache.DISTRICT_GRANULARITY)
|
|
cache.put("GJ1c", Vector2i(10, 20), 32, quarter_window, 4)
|
|
|
|
assert_int(cache.size()).is_equal(2)
|
|
assert_that(
|
|
cache.get_window("GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY)
|
|
).is_equal(district_window)
|
|
assert_that(cache.get_window("GJ1c", Vector2i(10, 20), 32, 4)).is_equal(quarter_window)
|
|
|
|
|
|
# =============================================================================
|
|
# granularity_v2 (T-1152/T-1153): the string-tag axis — the ONLY thing that
|
|
# distinguishes Region from District/Quarter, since Region has no legal
|
|
# legacy-int representation (WindowGranularity::legacy_u32() returns None for
|
|
# Region — see the server's own doc). This is the SAME mandatory-aliasing
|
|
# regression class as the granularity/min_wl_m tests above, extended to the
|
|
# new axis.
|
|
# =============================================================================
|
|
|
|
|
|
## **MANDATORY aliasing regression (T-1152/T-1153):** a Region-rung key and a
|
|
## District-rung key at the IDENTICAL (body_id, center, n, legacy
|
|
## granularity, min_wl_m) must be DISTINCT cache keys — the legacy int slot
|
|
## alone (both "District" and default-omitted callers pass
|
|
## DISTRICT_GRANULARITY=1) cannot tell them apart; granularity_v2 is what
|
|
## does.
|
|
func test_make_key_distinguishes_granularity_v2_region_from_district() -> void:
|
|
var k_district := AtlasWindowCache.make_key(
|
|
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
|
|
)
|
|
var k_region := AtlasWindowCache.make_key(
|
|
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "Region"
|
|
)
|
|
assert_str(k_district).is_not_equal(k_region)
|
|
|
|
|
|
## Omitting granularity_v2 (every pre-T-1152 call site) must produce the SAME
|
|
## key as passing the explicit "District" default — byte/string
|
|
## compatibility, same contract as the legacy granularity/min_wl_m defaults.
|
|
func test_omitted_granularity_v2_matches_explicit_district_default() -> void:
|
|
var k_omitted := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32)
|
|
var k_explicit := AtlasWindowCache.make_key(
|
|
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
|
|
)
|
|
assert_str(k_omitted).is_equal(k_explicit)
|
|
|
|
|
|
## End-to-end through put()/get_window(): a Region-rung window and a
|
|
## District-rung window at the identical (body, center, n) must both be
|
|
## independently retrievable — the exact scenario a full-zoom-out-then-back-in
|
|
## at the SAME (center, n) would hit if a player oscillates across the
|
|
## District/Region boundary.
|
|
func test_region_and_district_windows_coexist_at_identical_body_center_n() -> void:
|
|
var cache := AtlasWindowCache.new()
|
|
var district_window := {"granularity_v2": "District", "id": "district"}
|
|
var region_window := {"granularity_v2": "Region", "id": "region"}
|
|
|
|
cache.put(
|
|
"GJ1c", Vector2i(10, 20), 32, district_window, AtlasWindowCache.DISTRICT_GRANULARITY, 0,
|
|
"District"
|
|
)
|
|
cache.put(
|
|
"GJ1c", Vector2i(10, 20), 32, region_window, AtlasWindowCache.DISTRICT_GRANULARITY, 0,
|
|
"Region"
|
|
)
|
|
|
|
assert_int(cache.size()).is_equal(2)
|
|
assert_that(
|
|
cache.get_window(
|
|
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
|
|
)
|
|
).is_equal(district_window)
|
|
assert_that(
|
|
cache.get_window(
|
|
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "Region"
|
|
)
|
|
).is_equal(region_window)
|