## T-949 tests: atlas_viewer.gd's _load_markers Sol/non-Sol split and the ## CityNamesResponse handler. ## ## Covers the D-236 Sol guard (legacy synchronous markers.json read, kept ## byte-for-byte) and the async non-Sol path (a CityNamesRequest is queued ## instead of a direct file read; _markers only populates once the response ## arrives). Response shape pinned to dudley-atlas-server's contract ## (2026-07-14): {body_id, status, cities: [{city_id, name, is_capital}]}, ## with a SolExcluded status as the server-side Sol backstop. class_name TestAtlasCityNames extends GdUnitTestSuite const SOL_BODY_ID := "GJ0d" const SOL_SYSTEM_ID := "GJ-0" const NON_SOL_BODY_ID := "GJ903b" const NON_SOL_SYSTEM_ID := "GJ-903" ## A real Sol heightmap.png reference, resolved with the SAME formula ## _load_sol_markers_legacy() uses for a relative terrain_reference — computed ## once here (as an absolute path) so the test doesn't depend on hardcoding ## this checkout's location, and doesn't re-derive/guess the resolution logic ## separately from production. func _sol_heightmap_ref() -> String: var project_root: String = ( ProjectSettings.globalize_path("res://").get_base_dir().get_base_dir() ) return project_root + "/wiki/star-systems/GJ-0/bodies/GJ0d/heightmap.png" ## add_child fires _ready() synchronously (matches test_implant_app_lifecycle.gd's ## established pattern) — builds _city_panel/_overlay_node/etc. so show_body() ## doesn't null-deref. func _make_viewer() -> AtlasViewer: var v := AtlasViewer.new() add_child(v) return v func test_sol_body_uses_legacy_synchronous_markers_read() -> void: var v := _make_viewer() var body := {"body_id": SOL_BODY_ID, "terrain_reference": _sol_heightmap_ref()} var system := {"system_id": SOL_SYSTEM_ID} v.show_body(body, system) # The legacy path is synchronous — cities/rivers/etc. are populated # immediately, no bridge round-trip needed. var markers: Dictionary = v.get_markers() assert_bool(markers.has("cities")).override_failure_message( "Sol body must keep the legacy full-geometry markers.json read (D-236)" ).is_true() assert_int((markers.get("cities", []) as Array).size()).override_failure_message( "Sol's real markers.json must yield at least one city" ).is_greater(0) assert_bool(v.has_pending_city_names_request()).override_failure_message( "Sol bodies must never queue a CityNamesRequest (D-236/T-1073 exception)" ).is_false() v.queue_free() func test_non_sol_body_does_not_synchronously_populate_markers() -> void: var v := _make_viewer() v.show_body({"body_id": NON_SOL_BODY_ID}, {"system_id": NON_SOL_SYSTEM_ID}) # T-949: no more direct file read — markers stay empty until the async # CityNamesResponse arrives (never, in test mode — SimBridge has no server). assert_that(v.get_markers()).override_failure_message( "non-Sol bodies must not synchronously populate markers from a file read" ).is_equal({}) assert_bool(v.has_pending_city_names_request()).override_failure_message( "non-Sol bodies must queue a CityNamesRequest for their own body" ).is_true() v.queue_free() func test_city_names_received_ready_stores_under_dedicated_key() -> void: var v := _make_viewer() v.show_body({"body_id": NON_SOL_BODY_ID}, {"system_id": NON_SOL_SYSTEM_ID}) v._on_city_names_received( { "body_id": NON_SOL_BODY_ID, "status": "Ready", "cities": [{"city_id": 1, "name": "Ridgeback", "is_capital": false}], } ) # Stored under "city_names", NOT the legacy top-level "cities" key — # CityNameEntry has no position, so merging it into "cities" would make # _draw_cities()/_find_city_at() plot every entry at Vector2.ZERO. var markers: Dictionary = v.get_markers() assert_that(markers.get("cities", [])).override_failure_message( "non-Sol markers must NOT expose position-less entries under the top-level 'cities' key" ).is_equal([]) assert_int((markers.get("city_names", []) as Array).size()).is_equal(1) assert_str((markers.get("city_names", [])[0] as Dictionary).get("name")).is_equal("Ridgeback") assert_bool(v.has_pending_city_names_request()).is_false() v.queue_free() func test_city_names_received_sol_excluded_falls_back_to_legacy_read() -> void: var v := _make_viewer() # Body claims to be non-Sol at request time, but the server's own D-236 # backstop says otherwise — the defensive fallback must still work even # though this shouldn't happen given atlas_viewer.gd's own SOL_SYSTEM_ID # guard (belt-and-suspenders per dudley-atlas-server's contract note). v.show_body( {"body_id": SOL_BODY_ID, "terrain_reference": _sol_heightmap_ref()}, {"system_id": NON_SOL_SYSTEM_ID} ) assert_bool(v.has_pending_city_names_request()).is_true() v._on_city_names_received({"body_id": SOL_BODY_ID, "status": "SolExcluded", "cities": []}) var markers: Dictionary = v.get_markers() assert_bool(markers.has("cities")).override_failure_message( "SolExcluded must fall back to the legacy full-geometry markers.json read" ).is_true() assert_int((markers.get("cities", []) as Array).size()).is_greater(0) assert_bool(v.has_pending_city_names_request()).is_false() v.queue_free() func test_city_names_received_ignores_stale_body_response() -> void: var v := _make_viewer() v.show_body({"body_id": NON_SOL_BODY_ID}, {"system_id": NON_SOL_SYSTEM_ID}) # A response for a DIFFERENT body (the viewer navigated away while the # request was in flight) must not clobber state. v._on_city_names_received( {"body_id": "some_other_body", "status": "Ready", "cities": [{"name": "Nope"}]} ) assert_that(v.get_markers()).override_failure_message( "a stale-body CityNamesResponse must be ignored" ).is_equal({}) assert_bool(v.has_pending_city_names_request()).override_failure_message( "a stale-body response must not clear the real pending request" ).is_true() v.queue_free() func test_city_names_received_error_status_leaves_markers_empty() -> void: var v := _make_viewer() v.show_body({"body_id": NON_SOL_BODY_ID}, {"system_id": NON_SOL_SYSTEM_ID}) # Normalized string form — production handlers only ever see the output of # Protocol.city_names_response_from_raw, which reduces {"Error": msg} to # "Error" (review H5: the raw wire shape only passed by str() coincidence). v._on_city_names_received( {"body_id": NON_SOL_BODY_ID, "status": "Error", "cities": []} ) assert_that(v.get_markers()).override_failure_message( "an Error status must leave markers empty (#960/D-191's empty-markers case)" ).is_equal({}) assert_bool(v.has_pending_city_names_request()).is_false() v.queue_free() func test_connection_state_change_does_not_crash_or_clear_pending_marker() -> void: var v := _make_viewer() v.show_body({"body_id": NON_SOL_BODY_ID}, {"system_id": NON_SOL_SYSTEM_ID}) assert_bool(v.has_pending_city_names_request()).is_true() # SimBridge is in test_mode (no live connection) so request_city_names() # no-ops either way — this proves the handler doesn't crash, and that only # a real CityNamesResponse (not the mere state transition) clears the # pending marker. v._on_connection_state_changed( SimBridge.ConnectionState.CONNECTING, SimBridge.ConnectionState.CONNECTED ) assert_bool(v.has_pending_city_names_request()).is_true() v.queue_free()