From beee44b3c874d2e437ddf5429a58ceeae6647329 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 21:57:18 +0100 Subject: [PATCH] feat(client): add batch fixture, fixture smoke test, decode_errors test Tyre #1: Added Rust-generated input_batch_two.msgpack fixture for bidirectional D-030 Layer 1 symmetry (Vec). Tyre #2: Added all_fixtures_deserialize Rust test that reads every .msgpack fixture and verifies it deserializes (corruption guard). Hoshe #3: Added test_decode_snapshot_malformed_entities_counted test verifying the decode_errors counter on D-010 boundary violations. 45 client tests, 54 server tests pass. Co-Authored-By: Claude Opus 4.6 --- .../fixtures/msgpack/input_batch_two.msgpack | Bin 0 -> 48 bytes client/tests/test_protocol.gd | 35 +++++++++++++++ server/tests/gen_fixtures.rs | 16 +++++++ server/tests/serialization.rs | 41 ++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 client/tests/fixtures/msgpack/input_batch_two.msgpack diff --git a/client/tests/fixtures/msgpack/input_batch_two.msgpack b/client/tests/fixtures/msgpack/input_batch_two.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..4bdca573b8b2326ccf52c6e841280426e995cabc GIT binary patch literal 48 qcmbQ#w4@|6Ih$cwVsc4le%?yo{IXQP{GyT!RPhy_c_pbuKs5jXAQY+q literal 0 HcmV?d00001 diff --git a/client/tests/test_protocol.gd b/client/tests/test_protocol.gd index 0729afcfd..80004b695 100644 --- a/client/tests/test_protocol.gd +++ b/client/tests/test_protocol.gd @@ -184,6 +184,26 @@ func test_decode_snapshot_missing_fields() -> void: 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, + "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() @@ -251,6 +271,21 @@ func test_encode_player_inputs_empty() -> void: assert_that(raw.value.size()).is_equal(0) +# -- 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) + assert_that(raw.value[1]["action"]).is_equal("Interact") + + # -- Diagonal movement fixtures (D-030 Layer 1 cross-language) ----------------- func test_decode_diagonal_fixtures() -> void: diff --git a/server/tests/gen_fixtures.rs b/server/tests/gen_fixtures.rs index 3562393cd..3914094dc 100644 --- a/server/tests/gen_fixtures.rs +++ b/server/tests/gen_fixtures.rs @@ -115,6 +115,22 @@ fn generate_msgpack_fixtures() { &rmp_serde::to_vec_named(&snapshot_multi).unwrap(), ); + // Batch input: Vec with two actions (D-030 Layer 1 bidirectional symmetry) + let input_batch = vec![ + PlayerInput { + tick: 0, + action: PlayerAction::MoveNorth, + }, + PlayerInput { + tick: 0, + action: PlayerAction::Interact, + }, + ]; + write_fixture( + "input_batch_two", + &rmp_serde::to_vec_named(&input_batch).unwrap(), + ); + // Diagonal movement fixtures (clockwise: NE, SE, SW, NW) for (name, action) in [ ("input_move_northeast", PlayerAction::MoveNortheast), diff --git a/server/tests/serialization.rs b/server/tests/serialization.rs index af60a0d75..3755c3d29 100644 --- a/server/tests/serialization.rs +++ b/server/tests/serialization.rs @@ -1,6 +1,7 @@ //! IPC serialization round-trip tests (D-030 Layer 1: fixture-based). use settled_reach_server::bridge::types::*; +use std::fs; #[test] fn observer_snapshot_roundtrip() { @@ -82,6 +83,46 @@ fn all_player_action_variants_roundtrip() { } } +/// All .msgpack fixtures must deserialize without error (guards against corruption in git). +/// Snapshot fixtures deserialize as ObserverSnapshot, input_* as PlayerInput, +/// input_batch_* as Vec. +#[test] +fn all_fixtures_deserialize() { + let fixture_dir = std::path::Path::new("../client/tests/fixtures/msgpack"); + assert!( + fixture_dir.exists(), + "Fixture directory not found: {}", + fixture_dir.display() + ); + + let mut count = 0; + for entry in fs::read_dir(fixture_dir).expect("read fixture dir") { + let entry = entry.expect("read dir entry"); + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("msgpack") { + continue; + } + let name = path.file_stem().unwrap().to_str().unwrap().to_string(); + let bytes = fs::read(&path).unwrap_or_else(|_| panic!("read fixture {}", name)); + + if name.starts_with("snapshot") { + rmp_serde::from_slice::(&bytes) + .unwrap_or_else(|e| panic!("deserialize snapshot fixture {}: {}", name, e)); + } else if name.starts_with("input_batch") { + rmp_serde::from_slice::>(&bytes) + .unwrap_or_else(|e| panic!("deserialize batch input fixture {}: {}", name, e)); + } else if name.starts_with("input") { + rmp_serde::from_slice::(&bytes) + .unwrap_or_else(|e| panic!("deserialize input fixture {}: {}", name, e)); + } else { + panic!("unknown fixture naming convention: {}", name); + } + count += 1; + } + assert!(count > 0, "no fixtures found"); + eprintln!("Verified {} fixtures", count); +} + /// All EntityKind variants must survive MessagePack round-trip (D-030 Layer 1) #[test] fn all_entity_kind_variants_roundtrip() {