## D-030 Layer 1: Protocol bridge tests for ObserverSnapshot features. ## Validates player_stance (D-053) and player_inventory (D-065) decode, ## GameState storage, SimBridge test mode, input encoding for stance toggles, ## protocol version checks, and fixture round-trips. ## Spec refs: D-053, D-065, D-020, #449 class_name TestProtocolBridge extends GdUnitTestSuite const FIXTURE_DIR = "res://tests/fixtures/msgpack/" # TODO: Add .msgpack fixture files for v6 stance/inventory variations. # Current fixtures (snapshot_one_npc, snapshot_empty, etc.) only cover default # Walk stance with empty inventory. Missing fixtures: # - snapshot with player_stance = Sprint, Careful, Crouch # - snapshot with populated player_inventory (1-item, 9-item full grid) # - combined stance + inventory variations # Stance/inventory decode is tested via in-memory Messagepack.encode() above, # but regression-safe .msgpack fixtures are needed for bridge contract coverage. 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()) # -- Protocol version upgrade ------------------------------------------------- func test_protocol_version_is_8() -> void: assert_that(Protocol.PROTOCOL_VERSION).is_equal(8) func test_fixtures_at_protocol_version_8() -> void: # All regenerated fixtures should be at v8 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.version).is_equal(8) func test_rejects_version_6() -> void: var raw := {"tick": 1, "version": 6, "entities": []} var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_null() # -- player_stance decode (D-053) --------------------------------------------- func test_decode_player_stance_walk() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "player_stance": "Walk", "player_inventory": [], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_not_null() assert_that(snapshot.player_stance).is_equal("Walk") func test_decode_player_stance_sprint() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "player_stance": "Sprint", "player_inventory": [], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.player_stance).is_equal("Sprint") func test_decode_player_stance_careful() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "player_stance": "Careful", "player_inventory": [], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.player_stance).is_equal("Careful") func test_decode_player_stance_crouch() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "player_stance": "Crouch", "player_inventory": [], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.player_stance).is_equal("Crouch") func test_decode_player_stance_missing_defaults_to_walk() -> void: # v6 snapshot without player_stance → should default to "Walk" var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_not_null() assert_that(snapshot.player_stance).is_equal("Walk") # -- player_inventory decode (D-065) ------------------------------------------ func test_decode_empty_inventory() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "player_stance": "Walk", "player_inventory": [], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.player_inventory.size()).is_equal(0) func test_decode_smuggler_inventory_3_items() -> void: # D-065: smuggler carries 3 specific items var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "player_stance": "Walk", "player_inventory": [ {"item_id": 100, "name": "Manifest Copy", "slot": 0}, {"item_id": 101, "name": "Access Token", "slot": 1}, {"item_id": 102, "name": "Comm Log", "slot": 2}, ], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.player_inventory.size()).is_equal(3) assert_that(snapshot.player_inventory[0].item_id).is_equal(100) assert_that(snapshot.player_inventory[0].name).is_equal("Manifest Copy") assert_that(snapshot.player_inventory[0].slot).is_equal(0) assert_that(snapshot.player_inventory[1].name).is_equal("Access Token") assert_that(snapshot.player_inventory[1].slot).is_equal(1) assert_that(snapshot.player_inventory[2].name).is_equal("Comm Log") assert_that(snapshot.player_inventory[2].slot).is_equal(2) func test_decode_full_9_slot_inventory() -> void: # D-065: 3x3 grid = 9 slots universal var items: Array = [] for i in range(9): items.append({"item_id": 100 + i, "name": "Item %d" % i, "slot": i}) var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "player_inventory": items, } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.player_inventory.size()).is_equal(9) assert_that(snapshot.player_inventory[0].slot).is_equal(0) assert_that(snapshot.player_inventory[8].slot).is_equal(8) assert_that(snapshot.player_inventory[8].item_id).is_equal(108) func test_decode_inventory_missing_defaults_to_empty() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.player_inventory.size()).is_equal(0) func test_decode_inventory_skips_malformed_items() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "player_inventory": [ {"item_id": 100, "name": "Valid Item", "slot": 0}, {"broken": true}, # Missing item_id and name {"item_id": 101}, # Missing name {"name": "No ID"}, # Missing item_id {"item_id": 102, "name": "Also Valid", "slot": 3}, ], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) # Only items with both item_id and name should decode assert_that(snapshot.player_inventory.size()).is_equal(2) assert_that(snapshot.player_inventory[0].name).is_equal("Valid Item") assert_that(snapshot.player_inventory[1].name).is_equal("Also Valid") func test_decode_inventory_item_slot_defaults_to_zero() -> void: var raw := { "tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [], "player_inventory": [ {"item_id": 100, "name": "No Slot"}, ], } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot.player_inventory[0].slot).is_equal(0) # -- GameState: v6 field storage ----------------------------------------------- func test_game_state_stores_player_stance() -> void: GameState.apply_snapshot({"tick": 1, "entities": [], "player_stance": "Sprint"}) assert_that(GameState.player_stance).is_equal("Sprint") GameState.player_stance = "Walk" # Reset func test_game_state_stores_player_inventory() -> void: var inv := [ {"item_id": 100, "name": "Manifest Copy", "slot": 0}, {"item_id": 101, "name": "Access Token", "slot": 1}, ] GameState.apply_snapshot({"tick": 1, "entities": [], "player_inventory": inv}) assert_that(GameState.player_inventory.size()).is_equal(2) assert_that(GameState.player_inventory[0].name).is_equal("Manifest Copy") GameState.player_inventory = [] # Reset func test_game_state_clears_inventory_when_absent() -> void: var inv := [{"item_id": 100, "name": "Item", "slot": 0}] GameState.apply_snapshot({"tick": 1, "entities": [], "player_inventory": inv}) assert_that(GameState.player_inventory.size()).is_equal(1) GameState.apply_snapshot({"tick": 2, "entities": []}) assert_that(GameState.player_inventory.size()).is_equal(0) func test_game_state_stance_persists_when_absent() -> void: # Stance should NOT reset when field is missing — keep last known value GameState.apply_snapshot({"tick": 1, "entities": [], "player_stance": "Careful"}) assert_that(GameState.player_stance).is_equal("Careful") GameState.apply_snapshot({"tick": 2, "entities": []}) # Stance persists (no explicit reset to Walk when absent) assert_that(GameState.player_stance).is_equal("Careful") GameState.player_stance = "Walk" # Reset func test_game_state_defaults() -> void: # Default values before any snapshot var gs_stance = GameState.player_stance var gs_inv = GameState.player_inventory assert_that(gs_stance).is_equal("Walk") assert_that(gs_inv.size()).is_equal(0) # -- SimBridge test mode: v6 fields ------------------------------------------- func test_sim_bridge_test_snapshot_has_player_stance() -> void: SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() assert_that(snap.has("player_stance")).is_true() assert_that(snap.player_stance).is_equal("Walk") func test_sim_bridge_test_snapshot_has_player_inventory() -> void: SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() assert_that(snap.has("player_inventory")).is_true() assert_that(snap.player_inventory is Array).is_true() func test_sim_bridge_test_snapshot_version_8() -> void: SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() assert_that(snap.version).is_equal(8) # -- Fixture: v6 snapshots include new fields ---------------------------------- func test_fixture_snapshots_have_v6_defaults() -> void: # All regenerated fixtures should have player_stance=Walk and empty inventory for fixture_name in ["snapshot_one_npc", "snapshot_empty", "snapshot_player"]: var bytes = _load_fixture(fixture_name) var snapshot = Protocol.decode_snapshot(bytes) assert_that(snapshot).is_not_null() assert_that(snapshot.player_stance).is_equal("Walk") assert_that(snapshot.player_inventory.size()).is_equal(0) # -- Input encoding: stance toggle actions (D-053) ---------------------------- func test_encode_toggle_stance_up() -> void: var inputs: Array = [{"tick": 10, "action_name": "ToggleStanceUp"}] var bytes := Protocol.encode_player_inputs(inputs) var raw = Messagepack.decode(bytes) assert_that(raw.status == null).is_true() assert_that(raw.value[0]["action"]).is_equal("ToggleStanceUp") func test_encode_toggle_stance_down() -> void: var inputs: Array = [{"tick": 10, "action_name": "ToggleStanceDown"}] var bytes := Protocol.encode_player_inputs(inputs) var raw = Messagepack.decode(bytes) assert_that(raw.status == null).is_true() assert_that(raw.value[0]["action"]).is_equal("ToggleStanceDown") func test_encode_decode_stance_toggle_roundtrip() -> void: for action_name in ["ToggleStanceUp", "ToggleStanceDown"]: var bytes = Protocol.encode_player_input(50, action_name) 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(action_name) assert_that(decoded.action.data).is_null() # -- Integration: full v6 snapshot round-trip ---------------------------------- func test_full_v6_snapshot_decode() -> void: # Simulate a realistic v6 snapshot with all fields populated var raw := { "tick": 100, "version": Protocol.PROTOCOL_VERSION, "game_time": {"day": 1, "time_of_day": 720, "day_phase": "Evening", "tick_rate": "Full"}, "player_facing": "Southeast", "player_stance": "Careful", "player_inventory": [ {"item_id": 100, "name": "Manifest Copy", "slot": 0}, {"item_id": 101, "name": "Access Token", "slot": 1}, {"item_id": 102, "name": "Comm Log", "slot": 2}, ], "entities": [ {"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": "Player", "visibility": "Forward", "relationship": "Unknown", "observation": "Visible"}, ], "visible_tiles": [ {"x": 10, "y": 10, "z": 0, "visibility": "Forward", "tile_kind": "Floor"}, ], "nearby_interactions": [{ "entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}], }], "current_monologue": { "id": "test_001", "text": "Sova Transit. The usual crowd.", "duration_seconds": 4.0, }, } var encoded = Messagepack.encode(raw) var snapshot = Protocol.decode_snapshot(encoded.value) assert_that(snapshot).is_not_null() assert_that(snapshot.tick).is_equal(100) assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION) assert_that(snapshot.player_facing).is_equal("Southeast") assert_that(snapshot.player_stance).is_equal("Careful") assert_that(snapshot.player_inventory.size()).is_equal(3) assert_that(snapshot.entities.size()).is_equal(1) assert_that(snapshot.nearby_interactions.size()).is_equal(1) assert_that(snapshot.current_monologue).is_not_null() assert_that(snapshot.current_monologue.text).is_equal("Sova Transit. The usual crowd.") func test_full_v6_snapshot_to_game_state() -> void: var snapshot := { "tick": 50, "player_stance": "Crouch", "player_inventory": [ {"item_id": 200, "name": "Access Token", "slot": 4}, ], "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], } GameState.apply_snapshot(snapshot) assert_that(GameState.current_tick).is_equal(50) assert_that(GameState.player_stance).is_equal("Crouch") assert_that(GameState.player_inventory.size()).is_equal(1) assert_that(GameState.player_inventory[0].name).is_equal("Access Token") assert_that(GameState.player_inventory[0].slot).is_equal(4) assert_that(GameState.player_position).is_equal(Vector2(5, 5)) # Reset GameState.player_stance = "Walk" GameState.player_inventory = []