Files
settled-reach/client/tests/test_atlas_data_delivery.gd
T
jpmschweitzerandClaude Fable 5 845737617c feat(client): atlas roads/settlements overlays + legend + server-API data delivery (T-960, T-949)
T-960: gen_l2_roads (MaintenanceAuthority-colored polylines, rail styling, junction markers) + gen_l3_settlements (size-scaled markers, capital shape, name labels) overlays — cities render on generated bodies for the first time; left-side generation legend panel (D-226 item 3, data-driven per-overlay spec, implant component library); protocol.gd decodes road_graph/settlements + the two new response types. T-949: system_index/atlas_app/overview_screen migrated off the direct star_map_data.json read to StarMapRequest over the bridge (loading state + replay-on-connect, no silent file fallback); atlas_viewer _load_markers requests CityNamesResponse for non-Sol bodies; Sol keeps the legacy authored markers.json geometry read (D-236/T-1073, load-bearing guard). Lead fix: _send_star_map_request now carries the same guard as request_star_map — the autoload's _star_map_wanted leaked across gdUnit suites and the unguarded replay-on-CONNECTED crashed 8 pre-existing flow tests on a Nil bridge; reset_test_state clears the flag. Fixtures regenerated via gen_fixtures (road/settlement samples). Full suite 2946/2946.

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

237 lines
9.4 KiB
GDScript

## T-949 tests: Atlas data delivery migration — StarMapRequest/StarMapResponse
## and CityNamesRequest/CityNamesResponse (protocol.gd encode/decode,
## decode_inbound's frame classification, and SimBridge signal routing).
##
## Wire shapes pinned to dudley-atlas-server's contract (2026-07-14):
## AtlasLayerResponse gains road_graph/settlements (Option, "None until ready"
## like district_grid); StarMapResponse is {status, data} (data = a verbatim
## MessagePack re-encoding of star_map_data.json, unwrapped by
## star_map_response_from_raw so callers see {"nodes": [...]} directly);
## CityNamesResponse is {body_id, status, cities: [{city_id, name, is_capital}]}
## with a SolExcluded status as the server-side Sol backstop (D-236).
class_name TestAtlasDataDelivery
extends GdUnitTestSuite
# =============================================================================
# Encode/decode round-trips
# =============================================================================
func test_encode_star_map_request_carries_discriminator() -> void:
var bytes := Protocol.encode_star_map_request()
assert_int(bytes.size()).is_greater(0)
var decoded = Messagepack.decode(bytes)
assert_that(decoded.status == null).is_true()
assert_that(decoded.value.get("star_map")).is_equal(true)
func test_star_map_response_from_raw_unwraps_data_to_nodes() -> void:
var nodes := [
{"system_id": "GJ 71", "proper_name": "Tau Ceti"},
{"system_id": "GJ 0", "proper_name": "Sol"},
]
var raw := {"status": "Ready", "data": {"_meta": {"system_count": 2}, "nodes": nodes}}
var response: Variant = Protocol.star_map_response_from_raw(raw)
assert_that(response).is_not_null()
assert_str((response as Dictionary).get("status")).is_equal("Ready")
assert_that((response as Dictionary).get("nodes")).is_equal(nodes)
func test_star_map_response_from_raw_error_status_yields_no_nodes() -> void:
var raw := {"status": {"Error": "db unavailable"}, "data": null}
var response: Variant = Protocol.star_map_response_from_raw(raw)
assert_that(response).is_not_null()
assert_str((response as Dictionary).get("status")).is_equal("Error")
assert_str((response as Dictionary).get("error")).is_equal("db unavailable")
assert_that((response as Dictionary).get("nodes")).is_equal([])
func test_star_map_response_from_raw_rejects_shape_without_status() -> void:
assert_that(Protocol.star_map_response_from_raw({"data": {}})).is_null()
assert_that(Protocol.star_map_response_from_raw("not a dict")).is_null()
func test_decode_star_map_response_bytes_round_trip() -> void:
var raw := {"status": "Ready", "data": {"nodes": [{"system_id": "GJ 71"}]}}
var encoded = Messagepack.encode(raw)
var response: Variant = Protocol.decode_star_map_response(encoded.value)
assert_that(response).is_not_null()
assert_that((response as Dictionary).get("nodes")).is_equal(raw["data"]["nodes"])
func test_encode_city_names_request_carries_discriminator_and_body_id() -> void:
var bytes := Protocol.encode_city_names_request("GJ903b")
var decoded = Messagepack.decode(bytes)
assert_that(decoded.status == null).is_true()
assert_that(decoded.value.get("city_names")).is_equal(true)
assert_that(decoded.value.get("body_id")).is_equal("GJ903b")
func test_city_names_response_from_raw_round_trips_ready() -> void:
var cities := [{"city_id": 1, "name": "Ridgeback", "is_capital": false}]
var raw := {"body_id": "GJ903b", "status": "Ready", "cities": cities}
var response: Variant = Protocol.city_names_response_from_raw(raw)
assert_that(response).is_not_null()
assert_str((response as Dictionary).get("body_id")).is_equal("GJ903b")
assert_str((response as Dictionary).get("status")).is_equal("Ready")
assert_that((response as Dictionary).get("cities")).is_equal(cities)
func test_city_names_response_from_raw_sol_excluded() -> void:
var raw := {"body_id": "GJ0d", "status": "SolExcluded", "cities": []}
var response: Variant = Protocol.city_names_response_from_raw(raw)
assert_that(response).is_not_null()
assert_str((response as Dictionary).get("status")).is_equal("SolExcluded")
func test_city_names_response_from_raw_requires_body_id_and_status() -> void:
assert_that(Protocol.city_names_response_from_raw({"body_id": "GJ903b"})).is_null()
assert_that(Protocol.city_names_response_from_raw({"status": "Ready"})).is_null()
# =============================================================================
# decode_inbound classification — snapshot / atlas / starmap / citynames
# =============================================================================
func test_decode_inbound_classifies_snapshot() -> void:
var raw := {"tick": 1, "entities": []}
var encoded = Messagepack.encode(raw)
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
assert_str(inbound.kind).is_equal("snapshot")
func test_decode_inbound_classifies_atlas() -> void:
var raw := {"body_id": "GJ1c", "status": "Ready", "layer1": null, "district_grid": null}
var encoded = Messagepack.encode(raw)
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
assert_str(inbound.kind).is_equal("atlas")
func test_decode_inbound_classifies_starmap() -> void:
# StarMapResponse is the only kind with "data" and no "body_id".
var raw := {"status": "Ready", "data": {"nodes": [{"system_id": "GJ 71"}]}}
var encoded = Messagepack.encode(raw)
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
assert_str(inbound.kind).is_equal("starmap")
assert_that((inbound.value as Dictionary).get("nodes")).is_equal(raw["data"]["nodes"])
func test_decode_inbound_classifies_citynames() -> void:
# CityNamesResponse is the only kind with "cities" — must classify as
# citynames, NOT atlas, even though both carry status + body_id.
var raw := {"body_id": "GJ903b", "status": "Ready", "cities": [{"city_id": 1, "name": "Ridgeback"}]}
var encoded = Messagepack.encode(raw)
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
assert_str(inbound.kind).is_equal("citynames")
assert_that((inbound.value as Dictionary).get("body_id")).is_equal("GJ903b")
func test_atlas_response_road_graph_and_settlements_passthrough() -> void:
var road_graph := {
"nodes": [{"position": [1, 2], "kind": "Settlement", "city_id": 5}],
"edges":
[
{
"from": 0,
"to": 1,
"path": [[1, 2], [3, 4]],
"maintenance": "Trade",
"is_rail": false,
"named_route_id": null,
}
],
}
var settlements := {
"settlements":
[
{
"city_id": 5,
"name": "Ridgeback",
"position": [1, 2],
"size_class": "Minor",
"is_capital": false,
"is_port": false,
}
]
}
var raw := {
"body_id": "GJ1c",
"status": "Ready",
"road_graph": road_graph,
"settlements": settlements,
}
var decoded: Variant = Protocol.atlas_response_from_raw(raw)
assert_that(decoded).is_not_null()
assert_that((decoded as Dictionary).get("road_graph")).is_equal(road_graph)
assert_that((decoded as Dictionary).get("settlements")).is_equal(settlements)
func test_atlas_response_road_graph_and_settlements_default_null() -> void:
# A Ready response before road_graph/settlements land on a given server
# build must decode cleanly with both fields absent, not error.
var decoded: Variant = Protocol.atlas_response_from_raw({"body_id": "GJ1c", "status": "Ready"})
assert_that(decoded).is_not_null()
assert_that((decoded as Dictionary).get("road_graph")).is_null()
assert_that((decoded as Dictionary).get("settlements")).is_null()
# =============================================================================
# SimBridge routing — receive_bytes emits the right signal with the right value
# =============================================================================
func test_sim_bridge_routes_star_map_response_to_signal() -> void:
var received: Array = []
var handler := func(response: Dictionary) -> void: received.append(response)
SimBridge.star_map_received.connect(handler)
var raw := {"status": "Ready", "data": {"nodes": [{"system_id": "GJ 71", "proper_name": "Tau Ceti"}]}}
var encoded = Messagepack.encode(raw)
SimBridge.receive_bytes(encoded.value)
SimBridge.star_map_received.disconnect(handler)
assert_int(received.size()).is_equal(1)
assert_that(received[0].get("nodes")).is_equal(raw["data"]["nodes"])
func test_sim_bridge_routes_city_names_response_to_signal() -> void:
var received: Array = []
var handler := func(response: Dictionary) -> void: received.append(response)
SimBridge.city_names_received.connect(handler)
var raw := {
"body_id": "GJ903b",
"status": "Ready",
"cities": [{"city_id": 1, "name": "Ridgeback", "is_capital": false}],
}
var encoded = Messagepack.encode(raw)
SimBridge.receive_bytes(encoded.value)
SimBridge.city_names_received.disconnect(handler)
assert_int(received.size()).is_equal(1)
assert_that(received[0].get("body_id")).is_equal("GJ903b")
func test_sim_bridge_still_routes_snapshots_after_starmap_additions() -> void:
# Regression guard: adding the starmap/citynames branches must not steal
# ordinary snapshot frames (the 20 Hz hot path).
var raw := {"tick": 5, "entities": []}
var encoded = Messagepack.encode(raw)
SimBridge.receive_bytes(encoded.value)
assert_that(SimBridge._last_snapshot).is_not_null()
assert_int(SimBridge._last_snapshot.get("tick")).is_equal(5)
func test_request_star_map_is_noop_in_test_mode() -> void:
# SimBridge defaults to test_mode = true (SR_LIVE unset) — request_star_map
# must not error even with no live bridge/server.
SimBridge.request_star_map()
func test_request_city_names_is_noop_in_test_mode() -> void:
SimBridge.request_city_names("GJ903b")