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<PlayerInput>).
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 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 21:57:18 +01:00
co-authored by Claude Opus 4.6
parent 8d12776b5d
commit beee44b3c8
4 changed files with 92 additions and 0 deletions
Binary file not shown.
+35
View File
@@ -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<PlayerInput> 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:
+16
View File
@@ -115,6 +115,22 @@ fn generate_msgpack_fixtures() {
&rmp_serde::to_vec_named(&snapshot_multi).unwrap(),
);
// Batch input: Vec<PlayerInput> 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),
+41
View File
@@ -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<PlayerInput>.
#[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::<ObserverSnapshot>(&bytes)
.unwrap_or_else(|e| panic!("deserialize snapshot fixture {}: {}", name, e));
} else if name.starts_with("input_batch") {
rmp_serde::from_slice::<Vec<PlayerInput>>(&bytes)
.unwrap_or_else(|e| panic!("deserialize batch input fixture {}: {}", name, e));
} else if name.starts_with("input") {
rmp_serde::from_slice::<PlayerInput>(&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() {