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:
Binary file not shown.
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user