//! Integration tests for TcpBridge over TCP localhost (D-030 Layer 2: IPC roundtrip). 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::SimBridge; use settled_reach_server::simulation::time::{DayPhase, TickRate}; use std::net::{TcpListener, TcpStream}; use std::thread; #[test] fn snapshot_roundtrip_over_tcp() { // Bind listener first — port is guaranteed ready before spawning threads let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind"); let server_addr = listener.local_addr().expect("failed to get local address"); // Server thread: accept on pre-bound listener and send snapshot let server_handle = thread::spawn(move || { let bridge = TcpBridge::accept_on(listener).expect("failed to accept"); let snapshot = ObserverSnapshot { version: PROTOCOL_VERSION, tick: 42, 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![VisibleEntity { entity_id: 100, x: 10.5, y: 20.3, z: 0, kind: EntityKind::Npc, visibility: VisibilitySector::Forward, relationship: RelationshipState::Unknown, observation: EntityVisibility::Visible, tell_state: None, }], 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![], }; bridge .send_snapshot(&snapshot) .expect("failed to send snapshot"); }); // Client: connect and receive snapshot (no sleep needed — listener already bound) let stream = TcpStream::connect(server_addr).expect("failed to connect"); let mut reader = std::io::BufReader::new(stream); let payload = read_framed(&mut reader) .expect("failed to read frame") .expect("unexpected EOF"); let snapshot: ObserverSnapshot = rmp_serde::from_slice(&payload).expect("failed to deserialize"); assert_eq!(snapshot.tick, 42); assert_eq!(snapshot.entities.len(), 1); assert_eq!(snapshot.entities[0].entity_id, 100); assert_eq!(snapshot.entities[0].x, 10.5); assert_eq!(snapshot.entities[0].y, 20.3); server_handle.join().expect("server thread panicked"); } #[test] fn input_roundtrip_over_tcp() { let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind"); let server_addr = listener.local_addr().expect("failed to get local address"); let server_handle = thread::spawn(move || { let bridge = TcpBridge::accept_on(listener).expect("failed to accept"); // Non-blocking socket: retry until data arrives or timeout. let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); let inputs = loop { match bridge.receive_inputs() { Ok(inputs) if !inputs.is_empty() => break inputs, Ok(_) => { assert!( std::time::Instant::now() < deadline, "timed out waiting for inputs" ); thread::sleep(std::time::Duration::from_millis(1)); } Err(e) => panic!("failed to receive inputs: {}", e), } }; assert_eq!(inputs.len(), 2); assert_eq!(inputs[0].tick, 10); assert_eq!(inputs[1].tick, 11); inputs }); // Client: connect and send inputs let stream = TcpStream::connect(server_addr).expect("failed to connect"); let mut writer = std::io::BufWriter::new(stream); let inputs = vec![ PlayerInput { tick: 10, action: PlayerAction::MoveNorth, }, PlayerInput { tick: 11, action: PlayerAction::Interact { target_entity_id: None, verb: None, }, }, ]; let payload = rmp_serde::to_vec_named(&inputs).expect("failed to serialize"); write_framed(&mut writer, &payload).expect("failed to write frame"); // Drop writer to close connection and signal EOF to server drop(writer); let received_inputs = server_handle.join().expect("server thread panicked"); match &received_inputs[0].action { PlayerAction::MoveNorth => {} _ => panic!("expected MoveNorth action"), } match &received_inputs[1].action { PlayerAction::Interact { .. } => {} _ => panic!("expected Interact action"), } } #[test] fn tcp_bridge_eof_returns_error() { let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind"); let server_addr = listener.local_addr().expect("failed to get local address"); let server_handle = thread::spawn(move || { let bridge = TcpBridge::accept_on(listener).expect("failed to accept"); // Non-blocking socket: retry until we get Disconnected or timeout. let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); loop { match bridge.receive_inputs() { Ok(inputs) if inputs.is_empty() => { // WouldBlock — client hasn't disconnected yet, retry assert!( std::time::Instant::now() < deadline, "timed out waiting for EOF" ); thread::sleep(std::time::Duration::from_millis(1)); } Ok(inputs) => panic!("expected Disconnected error, got {} inputs", inputs.len()), Err(settled_reach_server::bridge::BridgeError::Disconnected) => break, Err(e) => panic!("expected Disconnected error, got: {}", e), } } }); // Client: connect and immediately disconnect without sending data let stream = TcpStream::connect(server_addr).expect("failed to connect"); drop(stream); server_handle.join().expect("server thread panicked"); }