## D-030 Layer 1: Cross-language MessagePack tests ## Validates that Protocol.gd correctly decodes fixtures generated by Rust (rmp_serde). ## Fixtures generated by: cargo test --test gen_fixtures -- --ignored class_name TestProtocol extends GdUnitTestSuite const FIXTURE_DIR = "res://tests/fixtures/msgpack/" func _load_fixture(name: String) -> PackedByteArray: var path = FIXTURE_DIR + name + ".msgpack" var file = FileAccess.open(path, FileAccess.READ) assert_that(file).is_not_null() return file.get_buffer(file.get_length()) # -- Snapshot decoding ---------------------------------------------------------- func test_decode_snapshot_one_npc() -> void: var bytes = _load_fixture("snapshot_one_npc") var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(42) assert_that(snapshot.entities.size()).is_equal(1) var entity = snapshot.entities[0] assert_that(entity.entity_id).is_equal(1) assert_float(entity.x).is_equal_approx(10.0, 0.001) assert_float(entity.y).is_equal_approx(20.0, 0.001) assert_that(entity.z).is_equal(0) assert_that(entity.kind.variant).is_equal("Npc") assert_that(entity.kind.data).is_null() func test_decode_snapshot_empty() -> void: var bytes = _load_fixture("snapshot_empty") var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(0) assert_that(snapshot.entities.size()).is_equal(0) func test_decode_snapshot_player() -> void: var bytes = _load_fixture("snapshot_player") var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(1) assert_that(snapshot.entities.size()).is_equal(1) var player = snapshot.entities[0] assert_that(player.entity_id).is_equal(100) assert_float(player.x).is_equal_approx(16.5, 0.001) assert_float(player.y).is_equal_approx(16.5, 0.001) assert_that(player.z).is_equal(0) assert_that(player.kind.variant).is_equal("Player") assert_that(player.kind.data).is_null() func test_decode_snapshot_multi_entity() -> void: var bytes = _load_fixture("snapshot_multi_entity") var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(999) assert_that(snapshot.entities.size()).is_equal(4) # Player at (16.5, 16.5, 0) var player = snapshot.entities[0] assert_that(player.entity_id).is_equal(1) assert_float(player.x).is_equal_approx(16.5, 0.001) assert_float(player.y).is_equal_approx(16.5, 0.001) assert_that(player.z).is_equal(0) assert_that(player.kind.variant).is_equal("Player") # NPC at (5, 10, 0) var npc = snapshot.entities[1] assert_that(npc.entity_id).is_equal(2) assert_float(npc.x).is_equal_approx(5.0, 0.001) assert_float(npc.y).is_equal_approx(10.0, 0.001) assert_that(npc.z).is_equal(0) assert_that(npc.kind.variant).is_equal("Npc") # Object at (15.5, 3, 1) var obj = snapshot.entities[2] assert_that(obj.entity_id).is_equal(3) assert_float(obj.x).is_equal_approx(15.5, 0.001) assert_float(obj.y).is_equal_approx(3.0, 0.001) assert_that(obj.z).is_equal(1) assert_that(obj.kind.variant).is_equal("Object") # Terrain at (0, 0, -1) var terrain = snapshot.entities[3] assert_that(terrain.entity_id).is_equal(4) assert_float(terrain.x).is_equal_approx(0.0, 0.001) assert_float(terrain.y).is_equal_approx(0.0, 0.001) assert_that(terrain.z).is_equal(-1) assert_that(terrain.kind.variant).is_equal("Terrain") # -- PlayerInput decoding ------------------------------------------------------- func test_decode_input_move_north() -> void: var bytes = _load_fixture("input_move_north") var input = Protocol.decode_player_input(bytes) assert_that(input).is_not_null() assert_that(input.tick).is_equal(100) assert_that(input.action.variant).is_equal("MoveNorth") assert_that(input.action.data).is_null() func test_decode_input_perception_mode() -> void: var bytes = _load_fixture("input_perception_mode") var input = Protocol.decode_player_input(bytes) assert_that(input).is_not_null() assert_that(input.tick).is_equal(200) assert_that(input.action.variant).is_equal("UsePerceptionMode") assert_that(input.action.data).is_equal("thermal") # -- PlayerInput encoding ------------------------------------------------------- func test_encode_decode_roundtrip_unit_variant() -> void: var bytes = Protocol.encode_player_input(50, "MoveEast") assert_that(bytes.size()).is_greater(0) var decoded = Protocol.decode_player_input(bytes) assert_that(decoded).is_not_null() assert_that(decoded.tick).is_equal(50) assert_that(decoded.action.variant).is_equal("MoveEast") assert_that(decoded.action.data).is_null() func test_encode_decode_roundtrip_data_variant() -> void: var bytes = Protocol.encode_player_input(75, "UsePerceptionMode", "infrared") assert_that(bytes.size()).is_greater(0) var decoded = Protocol.decode_player_input(bytes) assert_that(decoded).is_not_null() assert_that(decoded.tick).is_equal(75) assert_that(decoded.action.variant).is_equal("UsePerceptionMode") assert_that(decoded.action.data).is_equal("infrared") # -- Cross-language roundtrip: GDScript encode matches Rust decode --------------- func test_gdscript_encode_matches_rust_fixture() -> void: # Encode the same MoveNorth input as the Rust fixture var bytes = Protocol.encode_player_input(100, "MoveNorth") # Decode and verify the content matches the Rust fixture var rust_bytes = _load_fixture("input_move_north") var from_gd = Protocol.decode_player_input(bytes) var from_rust = Protocol.decode_player_input(rust_bytes) assert_that(from_gd.tick).is_equal(from_rust.tick) assert_that(from_gd.action.variant).is_equal(from_rust.action.variant) assert_that(from_gd.action.data).is_equal(from_rust.action.data) # -- Negative tests: malformed/truncated input ----------------------------------- func test_decode_snapshot_truncated_bytes() -> void: var truncated := PackedByteArray([0x82, 0xa4]) # Incomplete msgpack map var result = Protocol.decode_snapshot(truncated) assert_that(result).is_null() func test_decode_snapshot_wrong_type() -> void: # Encode an array instead of a map — should fail validation var encoded = Messagepack.encode([1, 2, 3]) var result = Protocol.decode_snapshot(encoded.value) assert_that(result).is_null() func test_decode_snapshot_missing_fields() -> void: # Map with wrong keys var encoded = Messagepack.encode({"foo": "bar"}) var result = Protocol.decode_snapshot(encoded.value) assert_that(result).is_null() func test_decode_snapshot_malformed_entities_counted() -> void: # Snapshot with one valid and one malformed entity — decode_errors should count the bad one var raw := { "tick": 7, "version": 23, "entities": [ {"entity_id": 1, "x": 5.0, "y": 10.0, "z": 0, "kind": "Npc"}, {"entity_id": 2, "broken": true}, # Missing required fields {"x": 1.0}, # Missing entity_id, y, z, kind ], } var encoded: Variant = Messagepack.encode(raw) assert_that(encoded.status).is_null() var snapshot: Variant = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(7) assert_that(snapshot.entities.size()).is_equal(1) # Only the valid entity assert_that(snapshot.decode_errors).is_equal(2) # Two malformed entities func test_decode_player_input_empty_bytes() -> void: var result = Protocol.decode_player_input(PackedByteArray()) assert_that(result).is_null() func test_encode_produces_nonempty_bytes() -> void: var bytes = Protocol.encode_player_input(1, "MoveNorth") assert_that(bytes.size()).is_greater(0) # -- Batch input encoding (Vec wire format) ----------------------- func test_encode_player_inputs_single() -> void: var inputs: Array = [{"tick": 10, "action_name": "MoveNorth"}] var bytes := Protocol.encode_player_inputs(inputs) assert_that(bytes.size()).is_greater(0) # Decode as raw msgpack — should be an array with one element var raw: Variant = Messagepack.decode(bytes) assert_that(raw.status).is_null() assert_that(raw.value is Array).is_true() assert_that(raw.value.size()).is_equal(1) assert_that(raw.value[0]["tick"]).is_equal(10) assert_that(raw.value[0]["action"]).is_equal("MoveNorth") func test_encode_player_inputs_multiple() -> void: var inputs: Array = [ {"tick": 1, "action_name": "MoveNorth"}, {"tick": 1, "action_name": "Interact"}, {"tick": 2, "action_name": "MoveSouthwest"}, ] var bytes := Protocol.encode_player_inputs(inputs) assert_that(bytes.size()).is_greater(0) var raw: Variant = Messagepack.decode(bytes) assert_that(raw.status).is_null() assert_that(raw.value.size()).is_equal(3) assert_that(raw.value[0]["action"]).is_equal("MoveNorth") # Interact is a struct variant: {"Interact": {"target_entity_id": null, "verb": null}} assert_that(raw.value[1]["action"] is Dictionary).is_true() assert_that(raw.value[1]["action"].has("Interact")).is_true() assert_that(raw.value[2]["action"]).is_equal("MoveSouthwest") func test_encode_player_inputs_with_data_variant() -> void: var inputs: Array = [ {"tick": 5, "action_name": "UsePerceptionMode", "action_data": "thermal"}, ] var bytes := Protocol.encode_player_inputs(inputs) assert_that(bytes.size()).is_greater(0) var raw: Variant = Messagepack.decode(bytes) assert_that(raw.status).is_null() assert_that(raw.value[0]["action"] is Dictionary).is_true() assert_that(raw.value[0]["action"]["UsePerceptionMode"]).is_equal("thermal") func test_encode_player_inputs_empty() -> void: var inputs: Array = [] var bytes := Protocol.encode_player_inputs(inputs) assert_that(bytes.size()).is_greater(0) var raw: Variant = Messagepack.decode(bytes) assert_that(raw.status).is_null() assert_that(raw.value is Array).is_true() assert_that(raw.value.size()).is_equal(0) # -- v2 snapshot decoding ------------------------------------------------------- func test_decode_snapshot_v2_full() -> void: var bytes = _load_fixture("snapshot_v2_full") var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(500) # game_time assert_that(snapshot.game_time).is_not_null() assert_that(snapshot.game_time.day).is_equal(1) assert_that(snapshot.game_time.time_of_day).is_equal(720) # player_facing (unit enum → bare string) assert_that(snapshot.player_facing).is_equal("Southeast") # entities with visibility sector assert_that(snapshot.entities.size()).is_equal(1) assert_that(snapshot.entities[0].visibility).is_equal("Forward") # visible_tiles assert_that(snapshot.visible_tiles.size()).is_equal(3) assert_that(snapshot.visible_tiles[0].x).is_equal(10) assert_that(snapshot.visible_tiles[0].visibility).is_equal("Forward") assert_that(snapshot.visible_tiles[1].visibility).is_equal("Peripheral") func test_existing_fixtures_have_v2_fields() -> void: # All fixtures are generated by fixture_snapshot() — verify decoder extracts v2+ fields for fixture_name in [ "snapshot_one_npc", "snapshot_empty", "snapshot_player", "snapshot_multi_entity" ]: var bytes = _load_fixture(fixture_name) var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() assert_that(snapshot.player_facing).is_equal("North") assert_that(snapshot.game_time).is_not_null() func test_multi_entity_visibility_sectors() -> void: # snapshot_multi_entity has: Player=Forward, Npc=Peripheral, Object=Forward, Terrain=Forward var bytes = _load_fixture("snapshot_multi_entity") var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot.entities[0].visibility).is_equal("Forward") assert_that(snapshot.entities[1].visibility).is_equal("Peripheral") assert_that(snapshot.entities[2].visibility).is_equal("Forward") assert_that(snapshot.entities[3].visibility).is_equal("Forward") # -- Batch input fixture (D-030 Layer 1 bidirectional symmetry) ---------------- func test_decode_batch_input_fixture() -> void: # Rust-generated Vec fixture — verifies bidirectional Layer 1 compatibility var bytes = _load_fixture("input_batch_two") var raw: Variant = Messagepack.decode(bytes) assert_that(raw.status).is_null() assert_that(raw.value is Array).is_true() assert_that(raw.value.size()).is_equal(2) assert_that(raw.value[0]["tick"]).is_equal(0) assert_that(raw.value[0]["action"]).is_equal("MoveNorth") assert_that(raw.value[1]["tick"]).is_equal(0) # Interact is a struct variant: {"Interact": {"target_entity_id": null, "verb": null}} assert_that(raw.value[1]["action"] is Dictionary).is_true() assert_that(raw.value[1]["action"].has("Interact")).is_true() # -- Diagonal movement fixtures (D-030 Layer 1 cross-language) ----------------- func test_decode_diagonal_fixtures() -> void: # All 4 diagonal fixtures: clockwise NE, SE, SW, NW var diagonals: Array = [ ["input_move_northeast", "MoveNortheast"], ["input_move_southeast", "MoveSoutheast"], ["input_move_southwest", "MoveSouthwest"], ["input_move_northwest", "MoveNorthwest"], ] for pair in diagonals: var bytes = _load_fixture(pair[0]) var input: Variant = Protocol.decode_player_input(bytes) assert_that(input).is_not_null() assert_that(input.tick).is_equal(100) assert_that(input.action.variant).is_equal(pair[1]) assert_that(input.action.data).is_null() # -- v23: BookmarkCatalog decode ----------------------------------------------- func test_decode_snapshot_with_bookmark_catalog() -> void: # Hand-built dict — fixture generation requires server work, skip round-trip (#614). var raw := { "tick": 1, "version": 23, "entities": [], "bookmark_catalog": { "bookmarks": [ { "id": "bm_tycoon_arion", "title": "The Arion Run", "subtitle": "Mid-range freight corridor", "flavor": "You have contacts. Use them.", "default_location": "loc_arion_prime", "allowed_locations": ["loc_arion_prime", "loc_vethis_station"], "allowed_locations_cultures": ["arion", "vethis"], "career": "tycoon", "starting_capital_tractus": 50000, }, ], }, } var encoded: Variant = Messagepack.encode(raw) assert_that(encoded.status).is_null() var snapshot: Variant = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_not_null() assert_that(snapshot.bookmark_catalog).is_not_null() var bmc: Dictionary = snapshot.bookmark_catalog assert_that(bmc.has("bookmarks")).is_true() assert_that(bmc["bookmarks"].size()).is_equal(1) var bm: Dictionary = bmc["bookmarks"][0] assert_that(bm["id"]).is_equal("bm_tycoon_arion") assert_that(bm["title"]).is_equal("The Arion Run") assert_that(bm["default_location"]).is_equal("loc_arion_prime") assert_that(bm["allowed_locations"].size()).is_equal(2) assert_that(bm["allowed_locations"][0]).is_equal("loc_arion_prime") assert_that(bm["allowed_locations_cultures"][1]).is_equal("vethis") assert_that(bm["career"]).is_equal("tycoon") assert_that(bm["starting_capital_tractus"]).is_equal(50000) func test_decode_snapshot_bookmark_catalog_fixture() -> void: # Cross-language round-trip: Rust-generated fixture (#614). var bytes = _load_fixture("snapshot_with_bookmark_catalog") var snapshot: Variant = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() assert_that(snapshot.bookmark_catalog).is_not_null() var bmc: Dictionary = snapshot.bookmark_catalog assert_that(bmc["bookmarks"].size()).is_greater(0) var bm: Dictionary = bmc["bookmarks"][0] assert_that(bm.has("id")).is_true() assert_that(bm.has("title")).is_true() assert_that(bm.has("allowed_locations")).is_true() assert_that(bm["career"]).is_equal("tycoon") func test_decode_snapshot_no_bookmark_catalog_is_null() -> void: # Snapshot without bookmark_catalog key → field should be null. var raw := { "tick": 2, "version": 23, "entities": [], } var encoded: Variant = Messagepack.encode(raw) var snapshot: Variant = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_not_null() assert_that(snapshot.bookmark_catalog).is_null() # -- v23: RequestBookmarkCatalog + ConfirmBookmark encoding -------------------- func test_encode_request_bookmark_catalog_roundtrip() -> void: var bytes := Protocol.encode_request_bookmark_catalog() assert_that(bytes.size()).is_greater(0) var raw: Variant = Messagepack.decode(bytes) assert_that(raw.status).is_null() assert_that(raw.value is Array).is_true() assert_that(raw.value.size()).is_equal(1) var entry: Dictionary = raw.value[0] assert_that(entry["action_name"]).is_equal("RequestBookmarkCatalog") assert_that(entry.get("action_data")).is_null() func test_encode_confirm_bookmark_roundtrip() -> void: var bytes := Protocol.encode_confirm_bookmark("bm_tycoon_arion", "loc_arion_prime") assert_that(bytes.size()).is_greater(0) var raw: Variant = Messagepack.decode(bytes) assert_that(raw.status).is_null() assert_that(raw.value is Array).is_true() var entry: Dictionary = raw.value[0] assert_that(entry["action_name"]).is_equal("ConfirmBookmark") var data: Dictionary = entry["action_data"] assert_that(data["bookmark_id"]).is_equal("bm_tycoon_arion") assert_that(data["starting_location_id"]).is_equal("loc_arion_prime") # -- Atlas layer-stream protocol (#969, D-225) --------------------------------- func test_decode_atlas_response_ready() -> void: var bytes := _load_fixture("atlas_response_ready") var resp = Protocol.decode_atlas_layer_response(bytes) assert_that(resp).is_not_null() assert_that(resp.body_id).is_equal("GJ1c") assert_that(resp.status).is_equal("Ready") assert_that(resp.layer1).is_not_null() assert_that(resp.layer1.river_network.river_cells.size()).is_equal(2) assert_that(resp.layer1.attractors.size()).is_equal(1) assert_that(resp.layer1.attractors[0].attractor_type).is_equal("CoastalAccess") assert_that(resp.layer1.attractors[0].sub_biome).is_equal("CoastalLowland") func test_decode_atlas_response_pending() -> void: var bytes := _load_fixture("atlas_response_pending") var resp = Protocol.decode_atlas_layer_response(bytes) assert_that(resp).is_not_null() assert_that(resp.status).is_equal("Pending") assert_that(resp.layer1).is_null() func test_decode_atlas_response_not_found() -> void: var bytes := _load_fixture("atlas_response_not_found") var resp = Protocol.decode_atlas_layer_response(bytes) assert_that(resp.status).is_equal("NotFound") ## PR #191 review, Hoshe 3: `atlas_response_ready_with_window.msgpack` had NO ## consumer anywhere in client/tests — regenerated by the T-1150 `granularity`/ ## `min_wl_m` field additions but nothing decoded it through the real IPC path. ## This is that consumer, matching the sibling `test_decode_atlas_response_*` ## tests' style/fixture-dir convention above: full decode_atlas_layer_response() ## round trip (not a hand-built Dictionary like test_atlas_data_delivery.gd's ## passthrough tests), confirming `district_window.granularity`/`.min_wl_m` ## (T-1150's two new echo fields) survive the real client decode path. func test_decode_atlas_response_ready_with_window() -> void: var bytes := _load_fixture("atlas_response_ready_with_window") var resp = Protocol.decode_atlas_layer_response(bytes) assert_that(resp).is_not_null() assert_that(resp.status).is_equal("Ready") assert_that(resp.district_window).is_not_null() var window: Dictionary = resp.district_window assert_that(window.get("center")).is_equal([10, -5]) assert_that(int(window.get("n"))).is_equal(2) assert_that(int(window.get("granularity"))).is_equal(1) assert_that(int(window.get("min_wl_m"))).is_equal(0) func test_snapshot_is_not_decoded_as_atlas_response() -> void: # Disambiguation: an ObserverSnapshot has no "status" key, so the atlas # decoder rejects it. receive_bytes relies on this to route correctly. var bytes := _load_fixture("snapshot_empty") assert_that(Protocol.decode_atlas_layer_response(bytes)).is_null() func test_encode_atlas_request_shape() -> void: var bytes := Protocol.encode_atlas_layer_request("GJ1c", "Topography") assert_that(bytes.size()).is_greater(0) var raw: Variant = Messagepack.decode(bytes) assert_that(raw.status).is_null() assert_that(raw.value is Dictionary).is_true() assert_that(raw.value["body_id"]).is_equal("GJ1c") assert_that(raw.value["up_to"]).is_equal("Topography") # A request is a map with no "status" — must not be mistaken for a response. assert_that(Protocol.decode_atlas_layer_response(bytes)).is_null() func test_decode_inbound_classifies_frames() -> void: # The receive-side classifier: snapshot vs atlas response, decoded once. var snap = Protocol.decode_inbound(_load_fixture("snapshot_empty")) assert_that(snap.kind).is_equal("snapshot") assert_that(snap.value).is_not_null() var atlas = Protocol.decode_inbound(_load_fixture("atlas_response_ready")) assert_that(atlas.kind).is_equal("atlas") assert_that(atlas.value.status).is_equal("Ready")