Files
settled-reach/client/tests/test_atlas_overlays.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

248 lines
9.3 KiB
GDScript

## Tests for the Phase-4 generation overlays in the Atlas viewer (#960, D-225).
## Referencing AtlasViewer forces both it and AtlasMarkerOverlay to compile, so
## this also guards against parse errors in the overlay rendering code.
class_name TestAtlasOverlays
extends GdUnitTestSuite
# atlas_legend_panel.gd has no class_name (matches atlas_overlay_bar.gd, review
# #8), so its GENERATION_LEGEND spec table is read off the preloaded Script
# resource rather than a global identifier.
const LegendPanelScript := preload("res://ui/implant/apps/atlas/atlas_legend_panel.gd")
func test_generation_overlays_registered() -> void:
var ids: Array = []
for d: Dictionary in AtlasViewer.OVERLAY_DEFS:
ids.append(d["id"])
assert_that(ids).contains(
["gen_l1_rivers", "gen_l1_basins", "gen_l1_attractors", "gen_district"]
)
## T-1046: the district/morphology grid round-trips through the viewer and the
## protocol decode surfaces it; referencing AtlasMarkerOverlay compiles the draw.
func test_district_grid_round_trips() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
assert_that(v.get_generation_district_grid()).is_null()
var grid := {
"cols": 2,
"rows": 1,
"morphology": PackedByteArray([8, 14]),
"elev_q": PackedByteArray([10, 90]),
}
v.set_generation_district_grid(grid)
assert_that(v.get_generation_district_grid()).is_equal(grid)
# The decoded response dict carries the district_grid key (protocol.gd, T-1046).
var decoded: Variant = Protocol.atlas_response_from_raw(
{"body_id": "GJ1c", "status": "Ready", "district_grid": grid}
)
assert_that((decoded as Dictionary).get("district_grid")).is_equal(grid)
func test_generation_data_round_trips() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
assert_that(v.get_generation_layer1()).is_null()
var mock := {
"body_id": "GJ1c",
"river_network": {"river_cells": [], "confluences": [], "mouths": []},
"drainage_basins": [],
"attractors": [],
}
v.set_generation_layer1(mock)
assert_that(v.get_generation_layer1()).is_equal(mock)
func test_implant_pending_start_stop() -> void:
var p: ImplantPending = auto_free(ImplantPending.new())
p.start("GENERATING LAYER 1")
assert_bool(p.visible).is_true()
p.stop()
assert_bool(p.visible).is_false()
# =============================================================================
# T-960: gen_l2_roads / gen_l3_settlements overlays
# =============================================================================
func test_l2_l3_overlays_registered() -> void:
var ids: Array = []
for d: Dictionary in AtlasViewer.OVERLAY_DEFS:
ids.append(d["id"])
assert_that(ids).contains(["gen_l2_roads", "gen_l3_settlements"])
## RoadGraphLayer shape pinned to dudley-atlas-server's contract (2026-07-14):
## nodes carry NO `degree` (trimmed as server-internal bookkeeping — the
## overlay derives it from edge endpoints instead, see _draw_gen_roads).
func test_road_graph_round_trips() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
assert_that(v.get_generation_road_graph()).is_null()
var road_graph := {
"nodes":
[
{"city_id": 1, "position": [10, 20], "kind": "Settlement"},
{"city_id": null, "position": [12, 22], "kind": "Waypoint"},
],
"edges":
[
{
"from": 0,
"to": 1,
"path": [[10, 20], [12, 22]],
"maintenance": "Administrative",
"is_rail": false,
"named_route_id": null,
},
],
}
v.set_generation_road_graph(road_graph)
assert_that(v.get_generation_road_graph()).is_equal(road_graph)
# Round-trips through the protocol decode too (T-1046 district_grid precedent).
var decoded: Variant = Protocol.atlas_response_from_raw(
{"body_id": "GJ1c", "status": "Ready", "road_graph": road_graph}
)
assert_that((decoded as Dictionary).get("road_graph")).is_equal(road_graph)
## SettlementLayer shape pinned to dudley-atlas-server's contract
## (2026-07-14): wrapped under "settlements" (not "cities"); size_class is
## categorical (Major/Standard/Minor), and is_capital is authored, not
## population-derived.
func test_settlements_round_trip() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
assert_that(v.get_generation_settlements()).is_null()
var settlements := {
"settlements":
[
{
"city_id": 1,
"name": "Ridgeback",
"position": [30, 40],
"size_class": "Minor",
"is_capital": false,
"is_port": false,
},
{
"city_id": 2,
"name": "Capital City",
"position": [35, 45],
"size_class": "Major",
"is_capital": true,
"is_port": true,
},
],
}
v.set_generation_settlements(settlements)
assert_that(v.get_generation_settlements()).is_equal(settlements)
var decoded: Variant = Protocol.atlas_response_from_raw(
{"body_id": "GJ1c", "status": "Ready", "settlements": settlements}
)
assert_that((decoded as Dictionary).get("settlements")).is_equal(settlements)
## Tier-2 replay: a REAL server-generated msgpack blob (server/tests/gen_fixtures.rs
## generate_atlas_layer_response_fixtures, regenerated 2026-07-14 with T-960's
## road_graph/settlements populated) decoded through the actual client path —
## the strongest check that protocol.gd's decode matches the server's wire
## encoding, not just a hand-authored Dictionary the client wrote itself.
func test_atlas_response_ready_fixture_decodes_road_graph_and_settlements() -> void:
var path := "res://tests/fixtures/msgpack/atlas_response_ready.msgpack"
var f := FileAccess.open(path, FileAccess.READ)
assert_that(f).is_not_null()
var bytes := f.get_buffer(f.get_length())
f.close()
var decoded: Variant = Protocol.decode_atlas_layer_response(bytes)
assert_that(decoded).is_not_null()
var response: Dictionary = decoded
assert_str(response.get("body_id")).is_equal("GJ1c")
assert_str(response.get("status")).is_equal("Ready")
var road_graph: Dictionary = response.get("road_graph")
assert_that(road_graph).is_not_null()
assert_int((road_graph.get("nodes", []) as Array).size()).is_equal(2)
var edges: Array = road_graph.get("edges", [])
assert_int(edges.size()).is_equal(1)
assert_str((edges[0] as Dictionary).get("maintenance")).is_equal("Administrative")
assert_bool((edges[0] as Dictionary).get("is_rail")).is_false()
var settlements: Dictionary = response.get("settlements")
assert_that(settlements).is_not_null()
var entries: Array = settlements.get("settlements", [])
assert_int(entries.size()).is_equal(2)
var capital: Dictionary = entries[0]
assert_str(capital.get("name")).is_equal("Port Aldren")
assert_str(capital.get("size_class")).is_equal("Major")
assert_bool(capital.get("is_capital")).is_true()
var minor: Dictionary = entries[1]
assert_str(minor.get("name")).is_equal("Farmstead Rell")
assert_str(minor.get("size_class")).is_equal("Minor")
assert_bool(minor.get("is_capital")).is_false()
# =============================================================================
# D-226 item 3: generation legend panel
# =============================================================================
## Data-driven claim check: every legend entry must reference a real overlay
## id, so a typo or a stale entry can't silently produce a dead legend
## section that never appears.
func test_generation_legend_entries_reference_real_overlay_ids() -> void:
var overlay_ids: Array = []
for d: Dictionary in AtlasViewer.OVERLAY_DEFS:
overlay_ids.append(d["id"])
for spec: Dictionary in LegendPanelScript.GENERATION_LEGEND:
assert_that(overlay_ids).contains([spec.get("overlay_id", "")])
assert_bool((spec.get("rows", []) as Array).is_empty()).override_failure_message(
"legend entry '%s' has no rows" % spec.get("title", "?")
).is_false()
## Every gen_* toggleable overlay should have at least one legend entry so
## toggling it on never shows silent, unexplained map content.
func test_every_gen_overlay_has_a_legend_entry() -> void:
var legend_overlay_ids: Array = []
for spec: Dictionary in LegendPanelScript.GENERATION_LEGEND:
legend_overlay_ids.append(spec.get("overlay_id", ""))
for d: Dictionary in AtlasViewer.OVERLAY_DEFS:
var overlay_id: String = d["id"]
if overlay_id.begins_with("gen_"):
assert_that(legend_overlay_ids).override_failure_message(
"overlay '%s' has no GENERATION_LEGEND entry" % overlay_id
).contains([overlay_id])
## Full-lifecycle test (add_child fires _ready(), which builds the legend
## panel) — invisible at rest, since no generation overlay starts active.
func test_legend_panel_hidden_by_default() -> void:
var v: AtlasViewer = AtlasViewer.new()
add_child(v)
assert_bool(v._legend_panel.visible).override_failure_message(
"legend panel must be invisible when no generation overlay is active"
).is_false()
v.queue_free()
## Toggling a gen_* overlay on must reveal the legend and populate its rows;
## toggling it back off must hide it again (invisible-when-not-needed, D-226).
func test_legend_panel_shows_active_overlay_and_hides_when_toggled_off() -> void:
var v: AtlasViewer = AtlasViewer.new()
add_child(v)
v.set_overlay_visible("gen_l3_settlements", true)
assert_bool(v._legend_panel.visible).override_failure_message(
"legend panel must show once a generation overlay is toggled on"
).is_true()
assert_int(v._legend_panel.get_implant_children().size()).override_failure_message(
"legend panel must have content rows once populated"
).is_greater(0)
v.set_overlay_visible("gen_l3_settlements", false)
assert_bool(v._legend_panel.visible).override_failure_message(
"legend panel must hide again once its only active overlay is toggled off"
).is_false()
v.queue_free()