//! Error handling integration tests (#85). //! //! Tests the three error categories: //! 1. Protocol errors: malformed input → SimError + server continues //! 2. Desync detection: state_hash field populated in snapshots //! 3. SimError wire format roundtrip use bevy_app::prelude::*; use settled_reach_server::bridge::framing::{read_framed, write_framed}; use settled_reach_server::bridge::tcp::TcpBridge; use settled_reach_server::bridge::types::*; use settled_reach_server::bridge::{BridgePlugin, BridgeResource, ServerRunning}; use settled_reach_server::knowledge::{KnowledgeGraph, KnowledgePlugin}; use settled_reach_server::npc::relationships::TrustEventQueue; use settled_reach_server::simulation::interaction::NearbyInteractionBuffer; use settled_reach_server::simulation::monologue::{MonologueBuffer, MonologueState}; use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; use settled_reach_server::simulation::SimulationPlugin; use std::io::{BufReader, BufWriter}; use std::net::{TcpListener, TcpStream}; use std::sync::{Arc, Barrier}; use std::thread; /// Acceptance test (#85): send a malformed message mid-session, assert the /// server emits a SimError in the next snapshot and continues running. /// /// Uses barriers to synchronize the server and client threads, ensuring /// the malformed data arrives before the server's receive_inputs call. #[test] fn malformed_input_produces_sim_error_and_server_continues() { let listener = TcpListener::bind("127.0.0.1:0").expect("bind listener"); let server_addr = listener.local_addr().expect("get local addr"); // Barriers for tick synchronization between server and client. // Each barrier is used once: client signals "data sent", server proceeds to tick. let barrier_tick1 = Arc::new(Barrier::new(2)); let barrier_tick2 = Arc::new(Barrier::new(2)); let barrier_tick3 = Arc::new(Barrier::new(2)); let b1_server = Arc::clone(&barrier_tick1); let b2_server = Arc::clone(&barrier_tick2); let b3_server = Arc::clone(&barrier_tick3); // Server thread let server_handle = thread::spawn(move || { let bridge = TcpBridge::accept_on(listener).expect("accept connection"); let mut app = App::new(); app.add_plugins(SimulationPlugin); app.add_plugins(BridgePlugin); app.add_plugins(KnowledgePlugin); app.init_resource::(); app.insert_resource(BridgeResource::new(bridge)); app.insert_resource(WalkabilityMap::new(32, 32, 1)); app.world_mut().spawn(( PlayerCharacter, TilePosition::new(16, 16, 0), KnowledgeGraph::new(), NearbyInteractionBuffer::default(), MonologueBuffer::default(), MonologueState::default(), )); // Tick 1: wait for client to send valid input, then process b1_server.wait(); app.update(); assert!( app.world().resource::().0, "server should be running after tick 1" ); // Tick 2: wait for client to send malformed input, then process b2_server.wait(); app.update(); assert!( app.world().resource::().0, "server must continue running after malformed input" ); // Tick 3: wait for client to send valid input, then process b3_server.wait(); app.update(); assert!( app.world().resource::().0, "server should still be running after tick 3" ); }); // Client: connect and interact with synchronization let stream = TcpStream::connect(server_addr).expect("client connect"); let mut reader = BufReader::new(stream.try_clone().expect("clone for reader")); let mut writer = BufWriter::new(stream); // --- Tick 1: send valid input --- let valid_inputs = vec![PlayerInput { tick: 0, action: PlayerAction::MoveNorth, }]; let payload = rmp_serde::to_vec_named(&valid_inputs).expect("serialize"); write_framed(&mut writer, &payload).expect("send valid input"); barrier_tick1.wait(); // Signal: valid input sent // Read tick 1 snapshot let snap1_bytes = read_framed(&mut reader) .expect("read snapshot 1") .expect("not EOF"); let snap1: ObserverSnapshot = rmp_serde::from_slice(&snap1_bytes).expect("deserialize snapshot 1"); assert!(snap1.sim_errors.is_empty(), "no errors expected on tick 1"); // --- Tick 2: send malformed input (properly framed but garbage payload) --- let garbage_payload: Vec = vec![0xFF, 0xFE, 0xFD, 0xFC, 0xAB, 0xCD, 0xEF]; write_framed(&mut writer, &garbage_payload).expect("send malformed input"); barrier_tick2.wait(); // Signal: malformed input sent // Read tick 2 snapshot — should contain SimError let snap2_bytes = read_framed(&mut reader) .expect("read snapshot 2") .expect("not EOF"); let snap2: ObserverSnapshot = rmp_serde::from_slice(&snap2_bytes).expect("deserialize snapshot 2"); assert!( !snap2.sim_errors.is_empty(), "sim_errors must contain the protocol error from malformed input" ); assert_eq!( snap2.sim_errors[0].kind, SimErrorKind::ProtocolError, "error kind must be ProtocolError" ); assert!( snap2.sim_errors[0] .message .contains("Malformed input frame") || snap2.sim_errors[0] .message .contains("Deserialization error"), "error message should describe the deserialization failure, got: {}", snap2.sim_errors[0].message, ); // --- Tick 3: send valid input again — server must still work --- let valid_inputs2 = vec![PlayerInput { tick: 2, action: PlayerAction::MoveSouth, }]; let payload2 = rmp_serde::to_vec_named(&valid_inputs2).expect("serialize"); write_framed(&mut writer, &payload2).expect("send valid input after error"); barrier_tick3.wait(); // Signal: valid input sent // Read tick 3 snapshot — no errors, server recovered let snap3_bytes = read_framed(&mut reader) .expect("read snapshot 3") .expect("not EOF"); let snap3: ObserverSnapshot = rmp_serde::from_slice(&snap3_bytes).expect("deserialize snapshot 3"); assert!( snap3.sim_errors.is_empty(), "no errors expected on tick 3 — server recovered" ); // Clean up drop(reader); drop(writer); server_handle .join() .expect("server thread should not panic"); } /// State hash is populated in every snapshot and is deterministic for same state. #[test] fn state_hash_populated_in_snapshot() { let listener = TcpListener::bind("127.0.0.1:0").expect("bind listener"); let server_addr = listener.local_addr().expect("get local addr"); let server_handle = thread::spawn(move || { let bridge = TcpBridge::accept_on(listener).expect("accept connection"); let mut app = App::new(); app.add_plugins(SimulationPlugin); app.add_plugins(BridgePlugin); app.add_plugins(KnowledgePlugin); app.init_resource::(); app.insert_resource(BridgeResource::new(bridge)); app.insert_resource(WalkabilityMap::new(32, 32, 1)); app.world_mut().spawn(( PlayerCharacter, TilePosition::new(16, 16, 0), KnowledgeGraph::new(), NearbyInteractionBuffer::default(), MonologueBuffer::default(), MonologueState::default(), )); // Run one tick app.update(); }); let stream = TcpStream::connect(server_addr).expect("client connect"); let mut reader = BufReader::new(stream.try_clone().expect("clone for reader")); let mut writer = BufWriter::new(stream); // Send empty input batch (no movement) let inputs: Vec = vec![]; let payload = rmp_serde::to_vec_named(&inputs).expect("serialize"); write_framed(&mut writer, &payload).expect("send empty input"); // Read snapshot let snap_bytes = read_framed(&mut reader) .expect("read snapshot") .expect("not EOF"); let snap: ObserverSnapshot = rmp_serde::from_slice(&snap_bytes).expect("deserialize snapshot"); assert!( snap.state_hash.is_some(), "state_hash must be populated in snapshot" ); assert_ne!( snap.state_hash.unwrap(), 0, "state_hash should be a non-trivial hash value" ); drop(reader); drop(writer); server_handle .join() .expect("server thread should not panic"); } /// SimError roundtrips through MessagePack serialization. #[test] fn sim_error_roundtrip() { let error = SimError { kind: SimErrorKind::ProtocolError, message: "test protocol error".into(), tick: 42, }; let bytes = rmp_serde::to_vec_named(&error).expect("serialize SimError"); let decoded: SimError = rmp_serde::from_slice(&bytes).expect("deserialize SimError"); assert_eq!(decoded.kind, SimErrorKind::ProtocolError); assert_eq!(decoded.message, "test protocol error"); assert_eq!(decoded.tick, 42); } /// SimErrorKind::Panic variant roundtrips. #[test] fn sim_error_panic_variant_roundtrip() { let error = SimError { kind: SimErrorKind::Panic, message: "simulation system panicked".into(), tick: 100, }; let bytes = rmp_serde::to_vec_named(&error).expect("serialize"); let decoded: SimError = rmp_serde::from_slice(&bytes).expect("deserialize"); assert_eq!(decoded.kind, SimErrorKind::Panic); assert_eq!(decoded.message, "simulation system panicked"); assert_eq!(decoded.tick, 100); } /// Snapshot with sim_errors populates correctly through serialization. #[test] fn snapshot_with_sim_errors_roundtrips() { use settled_reach_server::simulation::time::{DayPhase, TickRate}; let snapshot = ObserverSnapshot { version: PROTOCOL_VERSION, tick: 10, game_time: GameTime { day: 0, time_of_day: 0, day_phase: DayPhase::Morning, tick_rate: TickRate::Full, }, player_facing: FacingDirection::North, player_stance: MovementStance::default(), player_inventory: vec![], entities: vec![], visible_tiles: vec![], nearby_interactions: vec![], current_monologue: None, pending_recognitions: vec![], dialogue_response: None, blocked_entities: vec![], scan_events: vec![], sound_events: vec![], conversation_events: vec![], conversation_ended: vec![], follow_state: None, character_pressure: None, rng_seed: None, poi_list: vec![], examine_result: None, player_knowledge: None, save_result: None, triangle_crisis_events: vec![], state_hash: Some(0xDEADBEEF), debug_response: None, sim_errors: vec![SimError { kind: SimErrorKind::ProtocolError, message: "bad frame".into(), tick: 10, }], current_ticker: None, settings_response: None, }; let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize"); let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize"); assert_eq!(decoded.state_hash, Some(0xDEADBEEF)); assert_eq!(decoded.sim_errors.len(), 1); assert_eq!(decoded.sim_errors[0].kind, SimErrorKind::ProtocolError); assert_eq!(decoded.sim_errors[0].message, "bad frame"); }