//! IPC serialization round-trip tests (D-030 Layer 1: fixture-based). use settled_reach_server::bridge::types::*; use settled_reach_server::simulation::time::{DayPhase, TickRate}; use std::fs; /// Helper to create a minimal v2 snapshot for tests fn test_snapshot(tick: u64, entities: Vec) -> ObserverSnapshot { ObserverSnapshot { version: PROTOCOL_VERSION, tick, game_time: GameTime { day: 0, time_of_day: 0, day_phase: DayPhase::Morning, tick_rate: TickRate::Full, }, player_facing: FacingDirection::North, entities, visible_tiles: vec![], nearby_interactions: vec![], current_monologue: None, } } #[test] fn observer_snapshot_roundtrip() { let snapshot = test_snapshot( 42, vec![VisibleEntity { entity_id: 1, x: 10.0, y: 20.0, z: 0, kind: EntityKind::Npc, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, }], ); let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize"); let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); assert_eq!(decoded.version, PROTOCOL_VERSION); assert_eq!(decoded.tick, 42); assert_eq!(decoded.entities.len(), 1); assert_eq!(decoded.entities[0].entity_id, 1); } #[test] fn player_input_roundtrip() { let input = PlayerInput { tick: 100, action: PlayerAction::MoveNorth, }; let bytes = rmp_serde::to_vec_named(&input).expect("serialize"); let decoded: PlayerInput = rmp_serde::from_slice(&bytes).expect("deserialize"); assert_eq!(decoded.tick, 100); } #[test] fn empty_snapshot_roundtrip() { let snapshot = test_snapshot(0, vec![]); let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize"); let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); assert_eq!(decoded.tick, 0); assert!(decoded.entities.is_empty()); } /// All PlayerAction variants must survive MessagePack round-trip (D-030 Layer 1) #[test] fn all_player_action_variants_roundtrip() { let actions = vec![ PlayerAction::MoveNorth, PlayerAction::MoveSouth, PlayerAction::MoveEast, PlayerAction::MoveWest, PlayerAction::MoveNortheast, PlayerAction::MoveNorthwest, PlayerAction::MoveSoutheast, PlayerAction::MoveSouthwest, PlayerAction::Interact { target_entity_id: None, verb: None }, PlayerAction::UsePerceptionMode("thermal".to_string()), PlayerAction::Pause, PlayerAction::Unpause, ]; for action in actions { let input = PlayerInput { tick: 1, action: action.clone(), }; let bytes = rmp_serde::to_vec_named(&input).expect("serialize"); let decoded: PlayerInput = rmp_serde::from_slice(&bytes).expect("deserialize"); assert_eq!(decoded.tick, 1); // Verify the variant survived by re-serializing and comparing bytes let re_bytes = rmp_serde::to_vec_named(&decoded).expect("re-serialize"); assert_eq!(bytes, re_bytes, "round-trip mismatch for action variant"); } } /// 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") { let snap = rmp_serde::from_slice::(&bytes) .unwrap_or_else(|e| panic!("deserialize snapshot fixture {}: {}", name, e)); assert_eq!(snap.version, PROTOCOL_VERSION, "fixture {} has wrong version", name); } 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() { let kinds = vec![ EntityKind::Player, EntityKind::Npc, EntityKind::Object, EntityKind::Terrain, ]; for (i, kind) in kinds.into_iter().enumerate() { let entity = VisibleEntity { entity_id: i as u64, x: 0.0, y: 0.0, z: 0, kind, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, }; let snapshot = test_snapshot(0, vec![entity]); let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize"); let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); let re_bytes = rmp_serde::to_vec_named(&decoded).expect("re-serialize"); assert_eq!( bytes, re_bytes, "round-trip mismatch for EntityKind variant" ); } } /// v2 snapshot fields round-trip correctly #[test] fn snapshot_v2_fields_roundtrip() { let snapshot = ObserverSnapshot { version: PROTOCOL_VERSION, tick: 100, game_time: GameTime { day: 3, time_of_day: 720, day_phase: DayPhase::Evening, tick_rate: TickRate::Paused, }, player_facing: FacingDirection::Southeast, entities: vec![VisibleEntity { entity_id: 1, x: 5.5, y: 10.5, z: 0, kind: EntityKind::Player, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, }], visible_tiles: vec![ VisibleTile { x: 5, y: 10, z: 0, visibility: VisibilitySector::Forward, tile_kind: TileKind::Floor, }, VisibleTile { x: 6, y: 10, z: 0, visibility: VisibilitySector::Peripheral, tile_kind: TileKind::Wall, }, ], nearby_interactions: vec![], current_monologue: None, }; let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize"); let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); assert_eq!(decoded.version, PROTOCOL_VERSION); assert_eq!(decoded.game_time.day, 3); assert_eq!(decoded.game_time.time_of_day, 720); assert_eq!(decoded.game_time.day_phase, DayPhase::Evening); assert_eq!(decoded.game_time.tick_rate, TickRate::Paused); assert_eq!(decoded.player_facing, FacingDirection::Southeast); assert_eq!(decoded.visible_tiles.len(), 2); assert_eq!(decoded.visible_tiles[0].visibility, VisibilitySector::Forward); assert_eq!(decoded.visible_tiles[1].visibility, VisibilitySector::Peripheral); assert_eq!(decoded.entities[0].visibility, VisibilitySector::Forward); } /// Entity::to_bits() must roundtrip through from_bits() — guards against /// bevy version changes silently breaking wire IDs (Hoshe #12). #[test] fn entity_to_bits_roundtrip() { use bevy_ecs::entity::Entity; // Create entities via a World so we get valid index+generation pairs let mut world = bevy_ecs::world::World::new(); let e1 = world.spawn_empty().id(); let e2 = world.spawn_empty().id(); let e3 = world.spawn_empty().id(); // Despawn and respawn to get a higher generation world.despawn(e2); let e4 = world.spawn_empty().id(); for entity in [e1, e2, e3, e4] { let bits = entity.to_bits(); let restored = Entity::from_bits(bits); assert_eq!(entity, restored, "Entity::to_bits() roundtrip failed for {:?}", entity); } } /// PROTOCOL_VERSION constant matches snapshot version field #[test] fn protocol_version_constant_matches_snapshot() { let snapshot = test_snapshot(0, vec![]); assert_eq!(snapshot.version, PROTOCOL_VERSION); assert_eq!(PROTOCOL_VERSION, 5, "bump this assertion when protocol version changes"); } /// All FacingDirection variants round-trip #[test] fn all_facing_direction_variants_roundtrip() { let directions = [ FacingDirection::North, FacingDirection::Northeast, FacingDirection::East, FacingDirection::Southeast, FacingDirection::South, FacingDirection::Southwest, FacingDirection::West, FacingDirection::Northwest, ]; for dir in directions { let snapshot = ObserverSnapshot { version: PROTOCOL_VERSION, tick: 0, game_time: GameTime { day: 0, time_of_day: 0, day_phase: DayPhase::Morning, tick_rate: TickRate::Full, }, player_facing: dir, entities: vec![], visible_tiles: vec![], nearby_interactions: vec![], current_monologue: None, }; let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize"); let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); assert_eq!(decoded.player_facing, dir); } }