Files
settled-reach/client/tests/test_atlas_city_names.gd
T
jpmschweitzerandClaude Fable 5 3304ee30da fix(simulation): PR #176 review round — all 12 findings + 2 recommendations addressed
H1 demux: ShapeProbe defensive multi-shape rejection (union frames now Err, not first-match; +2 tests) and doc claim made honest. H2/T1 SystemIndex.reset_test_state() folded into SimBridge.reset_test_state() (load() inline per autoload rule) + has_pending_request() accessor. H3 no-op tests now assert the replay flag both directions. H4 retry test actually ingests a failure and asserts the retry semantic. H5 error fixture uses the normalized status string. H6 bridge_tcp e2e sends all five frame shapes over real TCP (star-map + city-names buffers asserted). H7 positive replay-on-CONNECTED test via the test_local_bridge test-mode-flip precedent (stub bridge captures + decodes the request bytes). H8/T2 stale PLACEHOLDER doc replaced with the confirmed contract. H9 is_capital doc matches the COALESCE reality. T-r1 demux ceiling written down (next shape = tagged envelope). T-r2 AtlasLayerResponse governance ceiling comment. Lead item: the four cargo-fmt-formatted files from the gate round are now committed (layer_proxy/plugin/bridge-mod/main). H10 note for the record: the 13 snapshot_*.msgpack fixtures in commit 845737617 were regenerated because they were stale against their own generator (pre-existing version-key removal) — verified harmless, no client reads that key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:31:15 +02:00

182 lines
7.1 KiB
GDScript

## 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()