Files
settled-reach/client/tests/test_browser_adapter.gd
T
jpmschweitzerandClaude Fable 5 90c562a6a3 feat(ui): T-1133 data browser — implant/browser app, six index+detail screens (D-254 SS4)
New implant app (app_path implant/browser, key B, fullscreen), sibling
to the Atlas per Jeroen's IA ruling: kind picker -> generic filterable
index (live search-mode typing) -> generic detail, parameterized per
kind, composed entirely from D-169 components. available_in_companion
left unset (default true) — the app appears in the companion shell
automatically via the generic-host seam, zero companion-side wiring.

browser_adapter.gd is the sole home of literal wire field names: maps
Oscar's BrowseResponse contract ({id, primary, secondary} index rows;
BrowseDetail enum-as-single-key-map) to view models for all six kinds,
folding join partners (system economy/factions/culture, corporation
presence, commodity production chains with nested Leontief inputs).
browse_protocol.gd split out of protocol.gd (max-file-lines);
sim_bridge gains browse_response_received + request_browse_index/detail.

Live-data catch: a present-but-NULL key (unnamed asteroid belt
proper_name) bypasses Dictionary.get fallbacks and rendered '<null>' —
_display_or() null-vs-absent helper applied across all six detail
mappers, 4 regression tests distinct from the absent-key cases.

43 gdUnit adapter cases; full suite 3074 green. Live-verified against
a real server + real systems.db: all six kinds Ready with real row
counts (301/3240/466/165/36/28), detail drill-down, NotFound on bogus
ids. Spawn-mode DB resolution issue found during verification is
pre-existing (cwd-relative data/systems.db) — server-side fix follows
separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:24:39 +02:00

503 lines
18 KiB
GDScript

class_name TestBrowserAdapter
extends GdUnitTestSuite
## Unit tests for browser_adapter.gd (T-1133, D-254 §4).
##
## Scope: the pure translation-layer logic between Oscar's T-1131
## BrowseRequest/BrowseResponse wire contract and the browser screens' view-
## models — kind metadata, index row extraction/filtering, and detail
## payload unwrap + per-kind field mapping. All exercised with plain
## Dictionary literals standing in for decoded msgpack (no live server, no
## SimBridge, no protocol.gd round-trip needed to cover this logic).
##
## Deliberately NOT covered here: the actual wire encode/decode round-trip
## (that is protocol.gd's own concern — encode_browse_request/
## browse_response_from_raw), and the screens' rendering of the adapter's
## output into ImplantPanel components (covered by the live make atlas
## verification per the ticket's tiering — the screens are thin renderers
## over what this adapter already validates).
const SCRIPT_PATH := "res://ui/implant/apps/browser/browser_adapter.gd"
func _adapter():
return load(SCRIPT_PATH)
# =============================================================================
# Kind metadata
# =============================================================================
func test_kind_order_has_six_entries() -> void:
var a = _adapter()
assert_int(a.KIND_ORDER.size()).is_equal(6)
func test_kind_order_matches_d254_v1_list_order() -> void:
# D-254 §4's v1 entity scope list order: star systems, bodies, stations,
# corporations, commodities, trait catalog.
var a = _adapter()
assert_array(a.KIND_ORDER).is_equal(
[
a.KIND_STAR_SYSTEM,
a.KIND_BODY,
a.KIND_STATION,
a.KIND_CORPORATION,
a.KIND_COMMODITY,
a.KIND_TRAIT_TEMPLATE,
]
)
func test_is_valid_kind_accepts_all_six() -> void:
var a = _adapter()
for kind: String in a.KIND_ORDER:
assert_bool(a.is_valid_kind(kind)).override_failure_message(
"%s should be a valid kind" % kind
).is_true()
func test_is_valid_kind_rejects_unknown_string() -> void:
var a = _adapter()
assert_bool(a.is_valid_kind("Spaceship")).is_false()
assert_bool(a.is_valid_kind("")).is_false()
func test_kind_label_every_kind_has_a_non_empty_label() -> void:
var a = _adapter()
for kind: String in a.KIND_ORDER:
assert_str(a.kind_label(kind)).override_failure_message(
"%s must have a display label" % kind
).is_not_empty()
func test_kind_label_unknown_kind_falls_back_to_the_kind_string() -> void:
var a = _adapter()
assert_str(a.kind_label("Nonsense")).is_equal("Nonsense")
# =============================================================================
# index_rows — Ready vs. non-Ready responses
# =============================================================================
func test_index_rows_ready_status_extracts_rows() -> void:
var a = _adapter()
var response := {
"kind": "StarSystem",
"status": "Ready",
"index": [
{"id": "GJ-1", "primary": "GJ-1", "secondary": "K-type"},
{"id": "GJ-2", "primary": "GJ-2", "secondary": "M-type"},
],
}
var rows: Array = a.index_rows(response)
assert_int(rows.size()).is_equal(2)
assert_str(rows[0]["id"]).is_equal("GJ-1")
assert_str(rows[0]["primary"]).is_equal("GJ-1")
assert_str(rows[0]["secondary"]).is_equal("K-type")
func test_index_rows_missing_secondary_defaults_to_empty_string_not_null() -> void:
var a = _adapter()
var response := {
"kind": "TraitTemplate",
"status": "Ready",
"index": [{"id": "tag_1", "primary": "Some Trait"}],
}
var rows: Array = a.index_rows(response)
assert_str(rows[0]["secondary"]).is_equal("")
func test_index_rows_error_status_returns_empty_array() -> void:
var a = _adapter()
var response := {"kind": "Body", "status": "Error", "index": null}
assert_array(a.index_rows(response)).is_empty()
func test_index_rows_not_found_status_returns_empty_array() -> void:
# D-254/Oscar's contract: Index requests never produce NotFound in
# practice, but the adapter must not crash or misbehave if one somehow
# arrives — treat any non-Ready status uniformly as "no rows".
var a = _adapter()
var response := {"kind": "Body", "status": "NotFound", "index": null}
assert_array(a.index_rows(response)).is_empty()
func test_index_rows_ready_but_index_field_not_an_array_returns_empty() -> void:
var a = _adapter()
var response := {"kind": "Body", "status": "Ready", "index": null}
assert_array(a.index_rows(response)).is_empty()
func test_index_rows_ready_with_genuinely_empty_list_returns_empty_array() -> void:
# The "unknown body -> Ready with empty list" convention (Oscar's
# message) must round-trip cleanly, not be conflated with an error.
var a = _adapter()
var response := {"kind": "Body", "status": "Ready", "index": []}
assert_array(a.index_rows(response)).is_empty()
# =============================================================================
# filter_rows — live search box
# =============================================================================
func _sample_rows() -> Array:
return [
{"id": "gate-corporation", "primary": "Gate Corporation", "secondary": "corporation"},
{"id": "vethara", "primary": "Vethara", "secondary": "combine"},
{"id": "free-traders", "primary": "Free Traders Syndic", "secondary": "syndic"},
]
func test_filter_rows_empty_query_returns_all_rows_unchanged() -> void:
var a = _adapter()
var rows := _sample_rows()
assert_array(a.filter_rows(rows, "")).is_equal(rows)
func test_filter_rows_matches_primary_case_insensitively() -> void:
var a = _adapter()
var filtered: Array = a.filter_rows(_sample_rows(), "vethara")
assert_int(filtered.size()).is_equal(1)
assert_str(filtered[0]["id"]).is_equal("vethara")
func test_filter_rows_matches_secondary_column() -> void:
var a = _adapter()
var filtered: Array = a.filter_rows(_sample_rows(), "syndic")
assert_int(filtered.size()).is_equal(1)
assert_str(filtered[0]["id"]).is_equal("free-traders")
func test_filter_rows_matches_id() -> void:
var a = _adapter()
var filtered: Array = a.filter_rows(_sample_rows(), "gate-corp")
assert_int(filtered.size()).is_equal(1)
assert_str(filtered[0]["id"]).is_equal("gate-corporation")
func test_filter_rows_no_match_returns_empty_array() -> void:
var a = _adapter()
assert_array(a.filter_rows(_sample_rows(), "nonexistent-zzz")).is_empty()
func test_filter_rows_partial_word_matches_across_multiple_rows() -> void:
var a = _adapter()
# "corp" is a substring of both "Gate Corporation" (primary) and
# "corporation" (secondary) — same row matches twice, must not duplicate.
var filtered: Array = a.filter_rows(_sample_rows(), "corp")
assert_int(filtered.size()).is_equal(1)
# =============================================================================
# response_status / response_error
# =============================================================================
func test_response_status_ready() -> void:
var a = _adapter()
assert_str(a.response_status({"status": "Ready"})).is_equal("Ready")
func test_response_status_not_a_dictionary_returns_empty_string() -> void:
var a = _adapter()
assert_str(a.response_status(null)).is_equal("")
assert_str(a.response_status("not a dict")).is_equal("")
func test_response_error_returns_message_only_for_error_status() -> void:
var a = _adapter()
var response := {"status": "Error", "error": "db unavailable"}
assert_str(a.response_error(response)).is_equal("db unavailable")
func test_response_error_empty_for_ready_status() -> void:
var a = _adapter()
var response := {"status": "Ready", "error": ""}
assert_str(a.response_error(response)).is_equal("")
func test_response_error_empty_for_not_found_status() -> void:
var a = _adapter()
var response := {"status": "NotFound", "error": ""}
assert_str(a.response_error(response)).is_equal("")
# =============================================================================
# detail_payload — BrowseDetail enum-variant unwrap
# =============================================================================
func test_detail_payload_unwraps_kind_keyed_variant() -> void:
var a = _adapter()
var response := {
"kind": "Commodity",
"status": "Ready",
"detail": {"Commodity": {"commodity_id": "fusion_fuel", "name": "FUSION FUEL"}},
}
var payload: Dictionary = a.detail_payload(response)
assert_str(payload.get("commodity_id", "")).is_equal("fusion_fuel")
assert_str(payload.get("name", "")).is_equal("FUSION FUEL")
func test_detail_payload_non_ready_status_returns_empty_dict() -> void:
var a = _adapter()
var response := {"kind": "Commodity", "status": "NotFound", "detail": null}
assert_that(a.detail_payload(response)).is_equal({})
func test_detail_payload_detail_field_not_a_dictionary_returns_empty_dict() -> void:
var a = _adapter()
var response := {"kind": "Commodity", "status": "Ready", "detail": null}
assert_that(a.detail_payload(response)).is_equal({})
func test_detail_payload_falls_back_to_single_key_map_when_kind_key_missing() -> void:
# Defensive path: some enum-variant encodings may not key exactly on the
# response's own "kind" string — a single-key map should still unwrap.
var a = _adapter()
var response := {
"kind": "TraitTemplate",
"status": "Ready",
"detail": {"SomeOtherVariantName": {"tag": "spice_market"}},
}
var payload: Dictionary = a.detail_payload(response)
assert_str(payload.get("tag", "")).is_equal("spice_market")
# =============================================================================
# detail_view — per-kind dispatch + field mapping
# =============================================================================
func test_detail_view_star_system_maps_expected_fields() -> void:
var a = _adapter()
var payload := {
"system_id": "GJ-1",
"proper_name": "Xin Chengdu",
"star_type": "K",
"population": 1200000,
}
var view: Dictionary = a.detail_view(a.KIND_STAR_SYSTEM, payload)
assert_str(view["name"]).is_equal("Xin Chengdu")
assert_str(view["subtitle"]).is_equal("STAR SYSTEM")
var rows: Array = view["rows"]
assert_bool(rows.size() > 0).is_true()
# Spot-check one row is present with the right value.
var found := false
for row: Dictionary in rows:
if row["label"] == "star type":
assert_str(row["value"]).is_equal("K")
found = true
assert_bool(found).override_failure_message("expected a 'star type' row").is_true()
func test_detail_view_star_system_falls_back_to_system_id_when_unnamed() -> void:
var a = _adapter()
var payload := {"system_id": "GJ-999"}
var view: Dictionary = a.detail_view(a.KIND_STAR_SYSTEM, payload)
assert_str(view["name"]).is_equal("GJ-999")
## Regression test for a bug found via live verification (2026-07-17): a
## real Body row (an unnamed asteroid belt, GJ0-belt) has proper_name as a
## PRESENT key with a NULL value (msgpack encodes a SQL NULL column that
## way; Dictionary.get(key, fallback)'s fallback only fires when the key is
## ABSENT, not when it's present-and-null) — the naive
## str(p.get("proper_name", p.get("body_id", "—"))) rendered the literal
## string "<null>" instead of falling back to body_id. Distinct from
## test_detail_view_star_system_falls_back_to_system_id_when_unnamed above,
## which only covers the ABSENT-key case (that test alone did not catch this
## bug — it needs its own present-but-null case).
func test_detail_view_falls_back_correctly_when_name_field_is_present_but_null() -> void:
var a = _adapter()
var payload := {"body_id": "GJ0-belt", "proper_name": null, "body_type": "asteroid_belt"}
var view: Dictionary = a.detail_view(a.KIND_BODY, payload)
assert_str(view["name"]).is_equal("GJ0-belt")
## Same present-but-null regression, for the "distance" row's unit-suffix
## special case (str(p.get("dist_ly", "—")) + " ly" would render "<null> ly"
## for Sol's own row, where dist_ly is a genuine NULL — distance from Earth
## to itself is meaningless, not zero).
func test_detail_view_star_system_distance_row_handles_null_dist_ly() -> void:
var a = _adapter()
var payload := {"system_id": "GJ 0", "proper_name": "Sol", "dist_ly": null}
var view: Dictionary = a.detail_view(a.KIND_STAR_SYSTEM, payload)
var distance_value := ""
for row: Dictionary in view["rows"]:
if row["label"] == "distance":
distance_value = row["value"]
assert_str(distance_value).is_equal("—")
## Same present-but-null regression, for the two array-summary text-block
## builders (_presence_summary/_production_chains_summary) — a null entry
## field must not render as "<null>" inside the summary line.
func test_detail_view_corporation_presence_entry_with_null_operation_omits_it_cleanly() -> void:
var a = _adapter()
var payload := {
"corp_id": "gate-corporation",
"presence": [
{"location_id": "GJ1b-S1", "location_type": "station", "primary_operation": null}
],
}
var view: Dictionary = a.detail_view(a.KIND_CORPORATION, payload)
assert_str(view["text"]).contains("GJ1b-S1 (station)")
assert_str(view["text"]).not_contains("<null>")
func test_detail_view_body_maps_expected_fields() -> void:
var a = _adapter()
var payload := {
"body_id": "GJ1c",
"proper_name": "Chengdu Prime",
"body_type": "planet",
"inhabited": true,
}
var view: Dictionary = a.detail_view(a.KIND_BODY, payload)
assert_str(view["name"]).is_equal("Chengdu Prime")
assert_str(view["subtitle"]).is_equal("CELESTIAL BODY")
var inhabited_value := ""
for row: Dictionary in view["rows"]:
if row["label"] == "inhabited":
inhabited_value = row["value"]
assert_str(inhabited_value).is_equal("yes")
func test_detail_view_station_maps_expected_fields() -> void:
var a = _adapter()
var payload := {
"station_id": "GJ1b-S1",
"proper_name": "Horizon Station",
"station_type": "horizon",
}
var view: Dictionary = a.detail_view(a.KIND_STATION, payload)
assert_str(view["name"]).is_equal("Horizon Station")
assert_str(view["subtitle"]).is_equal("STATION")
func test_detail_view_corporation_maps_expected_fields() -> void:
var a = _adapter()
var payload := {
"corp_id": "gate-corporation",
"proper_name": "Gate Corporation",
"corp_type": "corporation",
"shadow_economy_access": false,
}
var view: Dictionary = a.detail_view(a.KIND_CORPORATION, payload)
assert_str(view["name"]).is_equal("Gate Corporation")
assert_str(view["subtitle"]).is_equal("CORPORATION")
var shadow_value := ""
for row: Dictionary in view["rows"]:
if row["label"] == "shadow economy access":
shadow_value = row["value"]
assert_str(shadow_value).is_equal("no")
func test_detail_view_corporation_with_no_presence_yields_empty_text() -> void:
var a = _adapter()
var payload := {"corp_id": "gate-corporation", "presence": []}
var view: Dictionary = a.detail_view(a.KIND_CORPORATION, payload)
assert_str(view["text"]).is_equal("")
func test_detail_view_corporation_presence_array_summarized_into_text() -> void:
# CorporationDetail.presence: Vec<CorpPresenceEntry> (Oscar's T-1131
# message, 2026-07-17) — {location_id, location_type, primary_operation}.
var a = _adapter()
var payload := {
"corp_id": "gate-corporation",
"presence": [
{
"location_id": "GJ1b-S1",
"location_type": "station",
"primary_operation": "refining",
},
{"location_id": "GJ-2", "location_type": "system", "primary_operation": ""},
],
}
var view: Dictionary = a.detail_view(a.KIND_CORPORATION, payload)
assert_str(view["text"]).contains("GJ1b-S1 (station) — refining")
assert_str(view["text"]).contains("GJ-2 (system)")
func test_detail_view_commodity_maps_expected_fields_and_description_text() -> void:
var a = _adapter()
var payload := {
"commodity_id": "fusion_fuel",
"name": "FUSION FUEL",
"tier": "intermediate",
"description": "Refined isotopic fuel for gate-rated drives.",
}
var view: Dictionary = a.detail_view(a.KIND_COMMODITY, payload)
assert_str(view["name"]).is_equal("FUSION FUEL")
assert_str(view["subtitle"]).is_equal("COMMODITY")
assert_str(view["text"]).is_equal("Refined isotopic fuel for gate-rated drives.")
func test_detail_view_commodity_with_no_description_or_chains_yields_empty_text() -> void:
var a = _adapter()
var payload := {"commodity_id": "fusion_fuel", "description": "", "produced_by": []}
var view: Dictionary = a.detail_view(a.KIND_COMMODITY, payload)
assert_str(view["text"]).is_equal("")
func test_detail_view_commodity_produced_by_chains_summarized_into_text() -> void:
# CommodityDetail.produced_by: Vec<ProductionChainEntry> with nested
# inputs: Vec<ChainInputEntry> (Oscar's T-1131 message, 2026-07-17).
var a = _adapter()
var payload := {
"commodity_id": "fusion_fuel",
"description": "Refined isotopic fuel.",
"produced_by": [
{
"chain_id": "fusion_refining",
"output_quantity": 1.0,
"inputs": [
{"input_commodity_id": "raw_hydrogen", "quantity": 3.0},
{"input_commodity_id": "catalyst", "quantity": 0.5},
],
}
],
}
var view: Dictionary = a.detail_view(a.KIND_COMMODITY, payload)
# str() on a float keeps its decimal form (str(1.0) == "1.0", not "1") —
# asserting against that honest formatting rather than a rounded guess.
assert_str(view["text"]).contains("Refined isotopic fuel.")
assert_str(view["text"]).contains("fusion_refining (qty 1.0)")
assert_str(view["text"]).contains("3.0x raw_hydrogen")
assert_str(view["text"]).contains("0.5x catalyst")
func test_detail_view_trait_template_maps_expected_fields_and_cultural_description() -> void:
var a = _adapter()
var payload := {
"tag": "spice_market",
"label": "Spice Market",
"corridor_pool": "heritage",
"cultural_description": "A dense stall market smelling of scorched cardamom.",
}
var view: Dictionary = a.detail_view(a.KIND_TRAIT_TEMPLATE, payload)
assert_str(view["name"]).is_equal("Spice Market")
assert_str(view["subtitle"]).is_equal("TRAIT TEMPLATE")
assert_str(view["text"]).is_equal("A dense stall market smelling of scorched cardamom.")
func test_detail_view_unknown_kind_returns_empty_shape_not_a_crash() -> void:
var a = _adapter()
var view: Dictionary = a.detail_view("Nonsense", {"some": "payload"})
assert_str(view["name"]).is_equal("")
assert_array(view["rows"]).is_empty()
func test_detail_view_missing_field_renders_as_em_dash_placeholder() -> void:
var a = _adapter()
var view: Dictionary = a.detail_view(a.KIND_STATION, {"station_id": "GJ1b-S1"})
var docking_value := ""
for row: Dictionary in view["rows"]:
if row["label"] == "docking class":
docking_value = row["value"]
assert_str(docking_value).is_equal("—")