From 0f75c3194d6be0f0a1c9e03a52a5f2d170950fab Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 20:59:46 +0100 Subject: [PATCH] feat(client): add LocalBridge framing tests and diagonal fixtures 12 new tests covering framing roundtrips, cross-layer Protocol+framing integration, and diagonal wire mapping. 4 new diagonal movement fixtures generated from Rust for D-030 Layer 1 cross-language verification. 33/33 tests pass. Co-Authored-By: Claude Opus 4.6 --- .../msgpack/input_move_northeast.msgpack | 1 + .../msgpack/input_move_northwest.msgpack | 1 + .../msgpack/input_move_southeast.msgpack | 1 + .../msgpack/input_move_southwest.msgpack | 1 + client/tests/test_local_bridge.gd | 159 ++++++++++++++++++ client/tests/test_local_bridge.gd.uid | 1 + client/tests/test_protocol.gd | 20 +++ server/tests/gen_fixtures.rs | 11 ++ 8 files changed, 195 insertions(+) create mode 100644 client/tests/fixtures/msgpack/input_move_northeast.msgpack create mode 100644 client/tests/fixtures/msgpack/input_move_northwest.msgpack create mode 100644 client/tests/fixtures/msgpack/input_move_southeast.msgpack create mode 100644 client/tests/fixtures/msgpack/input_move_southwest.msgpack create mode 100644 client/tests/test_local_bridge.gd create mode 100644 client/tests/test_local_bridge.gd.uid diff --git a/client/tests/fixtures/msgpack/input_move_northeast.msgpack b/client/tests/fixtures/msgpack/input_move_northeast.msgpack new file mode 100644 index 000000000..a0786efcc --- /dev/null +++ b/client/tests/fixtures/msgpack/input_move_northeast.msgpack @@ -0,0 +1 @@ +‚¤tickd¦action­MoveNortheast \ No newline at end of file diff --git a/client/tests/fixtures/msgpack/input_move_northwest.msgpack b/client/tests/fixtures/msgpack/input_move_northwest.msgpack new file mode 100644 index 000000000..98b549ef4 --- /dev/null +++ b/client/tests/fixtures/msgpack/input_move_northwest.msgpack @@ -0,0 +1 @@ +‚¤tickd¦action­MoveNorthwest \ No newline at end of file diff --git a/client/tests/fixtures/msgpack/input_move_southeast.msgpack b/client/tests/fixtures/msgpack/input_move_southeast.msgpack new file mode 100644 index 000000000..33145e1d4 --- /dev/null +++ b/client/tests/fixtures/msgpack/input_move_southeast.msgpack @@ -0,0 +1 @@ +‚¤tickd¦action­MoveSoutheast \ No newline at end of file diff --git a/client/tests/fixtures/msgpack/input_move_southwest.msgpack b/client/tests/fixtures/msgpack/input_move_southwest.msgpack new file mode 100644 index 000000000..7500a3343 --- /dev/null +++ b/client/tests/fixtures/msgpack/input_move_southwest.msgpack @@ -0,0 +1 @@ +‚¤tickd¦action­MoveSouthwest \ No newline at end of file diff --git a/client/tests/test_local_bridge.gd b/client/tests/test_local_bridge.gd new file mode 100644 index 000000000..f25792a77 --- /dev/null +++ b/client/tests/test_local_bridge.gd @@ -0,0 +1,159 @@ +## D-030 Layer 2: LocalBridge framing and transport tests +## Tests 4-byte BE length-prefix framing and diagonal movement wire mapping. +class_name TestLocalBridge +extends GdUnitTestSuite + + +# -- Framing encode/decode roundtrip ------------------------------------------- + +func test_frame_encode_decode_roundtrip() -> void: + var payload := PackedByteArray([0xDE, 0xAD, 0xBE, 0xEF, 0x42]) + var framed := LocalBridge.frame_encode(payload) + + # Should be 4-byte header + 5-byte payload = 9 bytes + assert_that(framed.size()).is_equal(9) + + # Header should be big-endian length 5 + assert_that(framed[0]).is_equal(0) + assert_that(framed[1]).is_equal(0) + assert_that(framed[2]).is_equal(0) + assert_that(framed[3]).is_equal(5) + + var decoded: Variant = LocalBridge.frame_decode(framed) + assert_that(decoded).is_not_null() + assert_that(decoded.payload).is_equal(payload) + assert_that(decoded.remainder.size()).is_equal(0) + + +func test_frame_encode_empty_payload() -> void: + var payload := PackedByteArray() + var framed := LocalBridge.frame_encode(payload) + + # 4-byte header + 0-byte payload + assert_that(framed.size()).is_equal(4) + assert_that(framed[0]).is_equal(0) + assert_that(framed[1]).is_equal(0) + assert_that(framed[2]).is_equal(0) + assert_that(framed[3]).is_equal(0) + + var decoded: Variant = LocalBridge.frame_decode(framed) + assert_that(decoded).is_not_null() + assert_that(decoded.payload.size()).is_equal(0) + + +func test_frame_decode_with_remainder() -> void: + var payload1 := PackedByteArray([0x01, 0x02, 0x03]) + var payload2 := PackedByteArray([0x04, 0x05]) + var framed := LocalBridge.frame_encode(payload1) + framed.append_array(LocalBridge.frame_encode(payload2)) + + # Decode first message + var decoded: Variant = LocalBridge.frame_decode(framed) + assert_that(decoded).is_not_null() + assert_that(decoded.payload).is_equal(payload1) + assert_that(decoded.remainder.size()).is_greater(0) + + # Decode second message from remainder + var decoded2: Variant = LocalBridge.frame_decode(decoded.remainder) + assert_that(decoded2).is_not_null() + assert_that(decoded2.payload).is_equal(payload2) + assert_that(decoded2.remainder.size()).is_equal(0) + + +func test_frame_decode_incomplete_header() -> void: + var data := PackedByteArray([0x00, 0x00]) # Only 2 bytes, need 4 + var decoded: Variant = LocalBridge.frame_decode(data) + assert_that(decoded).is_null() + + +func test_frame_decode_incomplete_payload() -> void: + # Header says 10 bytes but only 3 bytes of payload + var data := PackedByteArray([0x00, 0x00, 0x00, 0x0A, 0x01, 0x02, 0x03]) + var decoded: Variant = LocalBridge.frame_decode(data) + assert_that(decoded).is_null() + + +func test_frame_encode_large_payload_length() -> void: + # Verify big-endian encoding of a length > 255 + var payload := PackedByteArray() + payload.resize(300) + payload.fill(0xAB) + var framed := LocalBridge.frame_encode(payload) + + # Length 300 = 0x0000012C in big-endian + assert_that(framed[0]).is_equal(0x00) + assert_that(framed[1]).is_equal(0x00) + assert_that(framed[2]).is_equal(0x01) + assert_that(framed[3]).is_equal(0x2C) + + var decoded: Variant = LocalBridge.frame_decode(framed) + assert_that(decoded).is_not_null() + assert_that(decoded.payload.size()).is_equal(300) + + +# -- Framing with Protocol messages (cross-layer) ----------------------------- + +func test_framed_protocol_snapshot_roundtrip() -> void: + # Encode a snapshot with Protocol, frame it, decode the frame, decode the snapshot + var snapshot_data := {"tick": 42, "entities": []} + var encoded: Variant = Messagepack.encode(snapshot_data) + assert_that(encoded.status).is_null() + + var framed := LocalBridge.frame_encode(encoded.value) + var decoded_frame: Variant = LocalBridge.frame_decode(framed) + assert_that(decoded_frame).is_not_null() + + var snapshot: Variant = Protocol.decode_snapshot(decoded_frame.payload) + assert_that(snapshot).is_not_null() + assert_that(snapshot.tick).is_equal(42) + assert_that(snapshot.entities.size()).is_equal(0) + + +func test_framed_protocol_input_roundtrip() -> void: + var encoded := Protocol.encode_player_input(99, "MoveNorth") + assert_that(encoded.size()).is_greater(0) + + var framed := LocalBridge.frame_encode(encoded) + var decoded_frame: Variant = LocalBridge.frame_decode(framed) + assert_that(decoded_frame).is_not_null() + + var input: Variant = Protocol.decode_player_input(decoded_frame.payload) + assert_that(input).is_not_null() + assert_that(input.tick).is_equal(99) + assert_that(input.action.variant).is_equal("MoveNorth") + + +# -- Diagonal movement wire mapping -------------------------------------------- + +func test_action_enum_to_wire_all_directions_clockwise() -> void: + # All 8 directions in clockwise order (N, NE, E, SE, S, SW, W, NW) + var expected: Array = [ + [InputMapper.Action.MOVE_NORTH, "MoveNorth"], + [InputMapper.Action.MOVE_NORTHEAST, "MoveNortheast"], + [InputMapper.Action.MOVE_EAST, "MoveEast"], + [InputMapper.Action.MOVE_SOUTHEAST, "MoveSoutheast"], + [InputMapper.Action.MOVE_SOUTH, "MoveSouth"], + [InputMapper.Action.MOVE_SOUTHWEST, "MoveSouthwest"], + [InputMapper.Action.MOVE_WEST, "MoveWest"], + [InputMapper.Action.MOVE_NORTHWEST, "MoveNorthwest"], + ] + + for pair in expected: + var wire_name := SimBridge._action_enum_to_wire(pair[0]) + assert_that(wire_name).is_equal(pair[1]) + + +func test_action_enum_to_wire_non_movement() -> void: + assert_that(SimBridge._action_enum_to_wire(InputMapper.Action.INTERACT)).is_equal("Interact") + assert_that(SimBridge._action_enum_to_wire(InputMapper.Action.USE_PERCEPTION_MODE)).is_equal("UsePerceptionMode") + assert_that(SimBridge._action_enum_to_wire(InputMapper.Action.PAUSE)).is_equal("Pause") + + +func test_action_enum_to_wire_open_menu_returns_empty() -> void: + var wire_name := SimBridge._action_enum_to_wire(InputMapper.Action.OPEN_MENU) + assert_that(wire_name).is_equal("") + + +func test_action_enum_to_wire_unknown_returns_empty() -> void: + var wire_name := SimBridge._action_enum_to_wire(9999) + assert_that(wire_name).is_equal("") diff --git a/client/tests/test_local_bridge.gd.uid b/client/tests/test_local_bridge.gd.uid new file mode 100644 index 000000000..41a6491fc --- /dev/null +++ b/client/tests/test_local_bridge.gd.uid @@ -0,0 +1 @@ +uid://xpl0rsixyh3y diff --git a/client/tests/test_protocol.gd b/client/tests/test_protocol.gd index e574bf318..d045f2f7c 100644 --- a/client/tests/test_protocol.gd +++ b/client/tests/test_protocol.gd @@ -167,3 +167,23 @@ func test_decode_player_input_empty_bytes() -> void: func test_encode_produces_nonempty_bytes() -> void: var bytes = Protocol.encode_player_input(1, "MoveNorth") assert_that(bytes.size()).is_greater(0) + + +# -- 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() diff --git a/server/tests/gen_fixtures.rs b/server/tests/gen_fixtures.rs index 4cd63aff2..c6c43847a 100644 --- a/server/tests/gen_fixtures.rs +++ b/server/tests/gen_fixtures.rs @@ -61,4 +61,15 @@ fn generate_msgpack_fixtures() { ], }; write_fixture("snapshot_multi_entity", &rmp_serde::to_vec_named(&snapshot_multi).unwrap()); + + // Diagonal movement fixtures (clockwise: NE, SE, SW, NW) + for (name, action) in [ + ("input_move_northeast", PlayerAction::MoveNortheast), + ("input_move_southeast", PlayerAction::MoveSoutheast), + ("input_move_southwest", PlayerAction::MoveSouthwest), + ("input_move_northwest", PlayerAction::MoveNorthwest), + ] { + let input = PlayerInput { tick: 100, action }; + write_fixture(name, &rmp_serde::to_vec_named(&input).unwrap()); + } }