feat(ci): IPC serialization fixture files and cross-language tests (#271)
Five new .msgpack fixtures: snapshot_minimal, snapshot_full, player_input_move, player_input_interact, malformed. Rust generator in gen_fixtures.rs, 7 Rust validation tests in serialization.rs, GDScript cross-language test in test_ipc_fixtures.gd (22 assertions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,6 @@ fn test_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
@@ -174,13 +173,15 @@ fn all_fixtures_deserialize() {
|
||||
} 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") {
|
||||
} else if name.starts_with("input") || name.starts_with("player_input") {
|
||||
rmp_serde::from_slice::<PlayerInput>(&bytes)
|
||||
.unwrap_or_else(|e| panic!("deserialize input fixture {}: {}", name, e));
|
||||
} else if name.starts_with("boundary_raw") {
|
||||
// Raw integer boundary fixtures (#472): single u64 values
|
||||
rmp_serde::from_slice::<u64>(&bytes)
|
||||
.unwrap_or_else(|e| panic!("deserialize boundary raw fixture {}: {}", name, e));
|
||||
} else if name == "malformed" {
|
||||
// Intentionally truncated — skip deserialization check, error handling tested elsewhere
|
||||
} else {
|
||||
panic!(
|
||||
"unknown fixture naming convention: {} — add a deserialization branch for this prefix",
|
||||
@@ -280,7 +281,6 @@ fn snapshot_v2_fields_roundtrip() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
@@ -384,7 +384,6 @@ fn all_facing_direction_variants_roundtrip() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
@@ -1394,7 +1393,7 @@ fn serde_default_fields_fill_in_when_missing_from_wire() {
|
||||
// `tell_state`, `follow_state`, `rng_seed`, `zone_id`, `object_type`, etc.
|
||||
// are all `#[serde(default)]` — they must default to None/empty when absent.
|
||||
let minimal_json = serde_json::json!({
|
||||
"version": 14,
|
||||
"version": 13,
|
||||
"tick": 42,
|
||||
"game_time": {
|
||||
"day": 0,
|
||||
@@ -1642,3 +1641,142 @@ fn nearby_interaction_object_type_roundtrip() {
|
||||
Some(ObjectType::Container)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// #271: Named fixture validation tests (D-030 Layer 1)
|
||||
//
|
||||
// These tests read the committed .msgpack files and assert specific field
|
||||
// values. They serve as the Rust side of cross-language verification — the
|
||||
// same fixtures are decoded by client/tests/test_ipc_fixtures.gd.
|
||||
// ============================================================
|
||||
|
||||
fn read_named_fixture(name: &str) -> Vec<u8> {
|
||||
let path = format!("../client/tests/fixtures/msgpack/{}.msgpack", name);
|
||||
fs::read(&path).unwrap_or_else(|e| panic!("failed to read fixture '{}': {}", name, e))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_snapshot_minimal_fields() {
|
||||
let bytes = read_named_fixture("snapshot_minimal");
|
||||
let snap: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize snapshot_minimal");
|
||||
|
||||
assert_eq!(snap.version, PROTOCOL_VERSION, "protocol version mismatch");
|
||||
assert_eq!(snap.tick, 0, "tick should be 0");
|
||||
assert_eq!(snap.entities.len(), 1, "should have exactly 1 entity");
|
||||
assert_eq!(snap.entities[0].entity_id, 1);
|
||||
assert!(
|
||||
matches!(snap.entities[0].kind, EntityKind::Player),
|
||||
"entity should be Player kind"
|
||||
);
|
||||
assert!(snap.current_monologue.is_none(), "no monologue in minimal");
|
||||
assert!(snap.dialogue_response.is_none(), "no dialogue in minimal");
|
||||
assert!(snap.player_inventory.is_empty(), "no inventory in minimal");
|
||||
assert!(snap.poi_list.is_empty(), "no POIs in minimal");
|
||||
assert!(snap.player_knowledge.is_none(), "no KG in minimal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_snapshot_full_fields() {
|
||||
let bytes = read_named_fixture("snapshot_full");
|
||||
let snap: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize snapshot_full");
|
||||
|
||||
assert_eq!(snap.version, PROTOCOL_VERSION, "protocol version mismatch");
|
||||
assert_eq!(snap.tick, 42, "tick should be 42");
|
||||
|
||||
// Monologue
|
||||
let monologue = snap.current_monologue.as_ref().expect("monologue absent");
|
||||
assert_eq!(monologue.id, "test_monologue_001");
|
||||
assert_eq!(
|
||||
monologue.text,
|
||||
"Something feels off about this place."
|
||||
);
|
||||
|
||||
// Dialogue
|
||||
let dialogue = snap.dialogue_response.as_ref().expect("dialogue absent");
|
||||
assert_eq!(dialogue.speaker_entity_id, 99);
|
||||
assert_eq!(dialogue.speaker_name, "Kael");
|
||||
|
||||
// Inventory
|
||||
assert_eq!(snap.player_inventory.len(), 1);
|
||||
assert_eq!(snap.player_inventory[0].name, "Forged Customs Cert");
|
||||
|
||||
// POIs
|
||||
assert_eq!(snap.poi_list.len(), 1);
|
||||
assert_eq!(snap.poi_list[0].poi_id, "docking_bay_7");
|
||||
|
||||
// Examine result
|
||||
let examine = snap.examine_result.as_ref().expect("examine_result absent");
|
||||
assert_eq!(examine.entity_id, 42);
|
||||
|
||||
// Player knowledge
|
||||
let kg = snap.player_knowledge.as_ref().expect("player_knowledge absent");
|
||||
assert_eq!(kg.entities.len(), 1);
|
||||
assert_eq!(kg.entities[0].name, "Kael");
|
||||
assert_eq!(kg.facts.len(), 1);
|
||||
assert_eq!(kg.facts[0].fact_id, "poi.docking_bay_7");
|
||||
|
||||
// RNG seed
|
||||
assert_eq!(snap.rng_seed, Some(0xDEADBEEF));
|
||||
|
||||
// Pending recognitions
|
||||
assert_eq!(snap.pending_recognitions.len(), 1);
|
||||
assert_eq!(snap.pending_recognitions[0].entity_id, 7);
|
||||
|
||||
// Blocked entities
|
||||
assert_eq!(snap.blocked_entities, vec![5u64, 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_player_input_move_fields() {
|
||||
let bytes = read_named_fixture("player_input_move");
|
||||
let input: PlayerInput =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize player_input_move");
|
||||
|
||||
assert_eq!(input.tick, 1, "tick should be 1");
|
||||
assert!(
|
||||
matches!(input.action, PlayerAction::MoveNorth),
|
||||
"action should be MoveNorth"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_player_input_interact_fields() {
|
||||
let bytes = read_named_fixture("player_input_interact");
|
||||
let input: PlayerInput =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize player_input_interact");
|
||||
|
||||
assert_eq!(input.tick, 2, "tick should be 2");
|
||||
match &input.action {
|
||||
PlayerAction::Interact {
|
||||
target_entity_id,
|
||||
verb,
|
||||
} => {
|
||||
assert_eq!(*target_entity_id, Some(99u64), "target_entity_id should be Some(99)");
|
||||
assert_eq!(
|
||||
verb.as_deref(),
|
||||
Some("Talk"),
|
||||
"verb should be Some(\"Talk\")"
|
||||
);
|
||||
}
|
||||
other => panic!("expected Interact, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_malformed_fails_deserialization() {
|
||||
let bytes = read_named_fixture("malformed");
|
||||
// Intentionally truncated — must NOT deserialize as ObserverSnapshot
|
||||
let result = rmp_serde::from_slice::<ObserverSnapshot>(&bytes);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"malformed fixture should fail to deserialize as ObserverSnapshot"
|
||||
);
|
||||
// Also must NOT deserialize as PlayerInput
|
||||
let result2 = rmp_serde::from_slice::<PlayerInput>(&bytes);
|
||||
assert!(
|
||||
result2.is_err(),
|
||||
"malformed fixture should fail to deserialize as PlayerInput"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user