//! Include-based coverage for client app startup reactor behavior. //! //! Scope: compile `client/src/app.rs` as a module with deterministic local //! stubs for capture/render dependencies, then exercise `new` + `run`. //! Targets: `client/src/app.rs`. //! Why: app orchestration branches should stay stable in CI without physical //! devices. mod handshake { #[derive(Default, Clone, Debug)] pub struct PeerCaps { pub camera: bool, pub microphone: bool, } pub async fn negotiate(_uri: &str) -> PeerCaps { PeerCaps { camera: std::env::var("LESAVKA_TEST_CAP_CAMERA").is_ok(), microphone: std::env::var("LESAVKA_TEST_CAP_MIC").is_ok(), } } } mod app_support { use super::handshake::PeerCaps; use std::time::Duration; #[derive(Clone, Copy, Debug)] pub enum CameraCodec { H264, } #[derive(Clone, Copy, Debug)] pub struct CameraConfig { pub codec: CameraCodec, pub width: u32, pub height: u32, pub fps: u32, } pub fn resolve_server_addr(_args: &[String], env_addr: Option<&str>) -> String { env_addr.unwrap_or("http://127.0.0.1:9").to_string() } pub fn camera_config_from_caps(caps: &PeerCaps) -> Option { if !caps.camera { return None; } Some(CameraConfig { codec: CameraCodec::H264, width: 1280, height: 720, fps: 30, }) } pub fn sanitize_video_queue(queue: Option) -> usize { queue.unwrap_or(64).max(1) } pub fn next_delay(delay: Duration) -> Duration { std::cmp::min(delay.saturating_mul(2), Duration::from_secs(8)) } } mod input { pub mod camera { use crate::app_support::CameraConfig; use lesavka_common::lesavka::VideoPacket; pub struct CameraCapture; impl CameraCapture { pub fn new(_source: Option<&str>, _cfg: Option) -> anyhow::Result { Ok(Self) } pub fn pull(&self) -> Option { None } } } pub mod microphone { use lesavka_common::lesavka::AudioPacket; pub struct MicrophoneCapture; impl MicrophoneCapture { pub fn new() -> anyhow::Result { Ok(Self) } pub fn pull(&self) -> Option { None } } } pub mod inputs { use lesavka_common::lesavka::{KeyboardReport, MouseReport}; use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; pub struct InputAggregator { _kbd_tx: Sender, _mou_tx: Sender, _dev_mode: bool, _paste_tx: Option>, } impl InputAggregator { pub fn new( dev_mode: bool, kbd_tx: Sender, mou_tx: Sender, paste_tx: Option>, ) -> Self { Self { _kbd_tx: kbd_tx, _mou_tx: mou_tx, _dev_mode: dev_mode, _paste_tx: paste_tx, } } pub fn new_with_capture_mode( dev_mode: bool, kbd_tx: Sender, mou_tx: Sender, paste_tx: Option>, _capture_remote_boot: bool, ) -> Self { Self::new(dev_mode, kbd_tx, mou_tx, paste_tx) } pub fn init(&mut self) -> anyhow::Result<()> { Ok(()) } pub async fn run(&mut self) -> anyhow::Result<()> { std::future::pending::<()>().await; #[allow(unreachable_code)] Ok(()) } } } } mod output { pub mod audio { use lesavka_common::lesavka::AudioPacket; pub struct AudioOut; impl AudioOut { pub fn new() -> anyhow::Result { Ok(Self) } pub fn push(&self, _pkt: AudioPacket) {} } } pub mod video { use lesavka_common::lesavka::VideoPacket; pub struct MonitorWindow; pub struct UnifiedMonitorWindow; impl MonitorWindow { pub fn new(_id: u32) -> anyhow::Result { Ok(Self) } pub fn push_packet(&self, _pkt: VideoPacket) {} } impl UnifiedMonitorWindow { pub fn new() -> anyhow::Result { Ok(Self) } pub fn push_packet(&self, _pkt: VideoPacket) {} } } } mod paste { use anyhow::bail; use lesavka_common::lesavka::PasteRequest; pub fn build_paste_request(text: &str) -> anyhow::Result { if text == "bad" { bail!("synthetic paste build failure"); } Ok(PasteRequest { nonce: vec![], data: text.as_bytes().to_vec(), encrypted: true, }) } } #[path = "../../client/src/app.rs"] #[allow(warnings)] mod app_include_contract; mod tests { use super::app_include_contract::{ LesavkaClientApp, keyboard_stream_report, mouse_stream_report, }; use lesavka_common::lesavka::{KeyboardReport, MouseReport}; use serial_test::serial; use temp_env::with_var; use tokio_stream::wrappers::errors::BroadcastStreamRecvError; #[test] #[serial] fn run_headless_reaches_pending_reactor_branch() { with_var("LESAVKA_HEADLESS", Some("1"), || { with_var("LESAVKA_SERVER_ADDR", Some("http://127.0.0.1:9"), || { with_var("LESAVKA_TEST_CAP_CAMERA", None::<&str>, || { with_var("LESAVKA_TEST_CAP_MIC", None::<&str>, || { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let mut app = LesavkaClientApp::new().expect("new app"); let result = tokio::time::timeout( std::time::Duration::from_millis(80), app.run(), ) .await; assert!(result.is_err(), "headless run should stay pending"); }); }); }); }); }); } #[test] #[serial] fn run_non_headless_starts_stream_tasks_with_stubbed_caps() { with_var("LESAVKA_HEADLESS", None::<&str>, || { with_var("LESAVKA_SERVER_ADDR", Some("http://127.0.0.1:9"), || { with_var("LESAVKA_TEST_CAP_CAMERA", Some("1"), || { with_var("LESAVKA_TEST_CAP_MIC", Some("1"), || { let rt = tokio::runtime::Runtime::new().expect("runtime"); rt.block_on(async { let mut app = LesavkaClientApp::new().expect("new app"); let result = tokio::time::timeout( std::time::Duration::from_millis(120), app.run(), ) .await; assert!( result.is_err(), "run should stay in the central reactor loop" ); }); }); }); }); }); } #[test] fn keyboard_stream_report_turns_lag_into_a_clean_reset() { let pkt = keyboard_stream_report(Err(BroadcastStreamRecvError::Lagged(3))) .expect("lagged keyboard item should produce reset"); assert_eq!(pkt.data, vec![0; 8]); let passthrough = keyboard_stream_report(Ok(KeyboardReport { data: vec![1, 2, 3], })) .expect("ok keyboard item should pass through"); assert_eq!(passthrough.data, vec![1, 2, 3]); } #[test] fn mouse_stream_report_turns_lag_into_a_neutral_packet() { let pkt = mouse_stream_report(Err(BroadcastStreamRecvError::Lagged(5))) .expect("lagged mouse item should produce neutral packet"); assert_eq!(pkt.data, vec![0; 4]); let passthrough = mouse_stream_report(Ok(MouseReport { data: vec![9, 8, 7, 6], })) .expect("ok mouse item should pass through"); assert_eq!(passthrough.data, vec![9, 8, 7, 6]); } }