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>
182 lines
7.1 KiB
GDScript
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()
|