Files
settled-reach/client/tests/test_protocol_bridge.gd
T
jpmschweitzerandClaude Opus 4.6 674c7147de feat(client): sync protocol to v8 — dialogue_response + Interact encoding
Bump PROTOCOL_VERSION from 7 to 8 to match server. Three changes:

1. Add dialogue_response field decoding (DialogueResponseEvent with
   line_id, text, speaker_entity_id) from server #305/D-028.

2. Fix Interact encoding: server changed PlayerAction::Interact from
   unit variant to struct variant with {target_entity_id, verb}.
   Extract _encode_action() helper to handle this consistently.

3. Update all test assertions that checked version=7 or expected
   Interact as a bare string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:41:27 +01:00

404 lines
14 KiB
GDScript

## 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 = []