//! 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: 4, 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![], } } #[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, 4); 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, 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, 4, "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: 4, 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, }, VisibleTile { x: 6, y: 10, z: 0, visibility: VisibilitySector::Peripheral, }, ], nearby_interactions: vec![], }; let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize"); let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); assert_eq!(decoded.version, 4); 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); } /// 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: 4, 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![], }; 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); } }