//! Include-based coverage for client app startup reactor behavior. //! //! Scope: compile the client app reactor with deterministic local stubs for //! capture/render dependencies, then exercise `new` + `run`. //! Targets: `client/src/app.rs`, `client/src/app/downlink_media.rs`. //! Why: app orchestration branches should stay stable in CI without physical //! devices. mod handshake { #[allow(dead_code)] #[derive(Default, Clone, Debug)] pub struct PeerCaps { pub camera: bool, pub microphone: bool, pub bundled_webcam_media: bool, pub server_version: Option, } 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(), bundled_webcam_media: std::env::var("LESAVKA_TEST_CAP_BUNDLED").is_ok(), server_version: None, } } } #[allow(warnings)] mod live_capture_clock { include!("support/live_capture_clock_shim.rs"); } #[path = "../../client/src/uplink_fresh_queue.rs"] #[allow(warnings)] mod uplink_fresh_queue; #[path = "../../client/src/uplink_telemetry.rs"] #[allow(warnings)] mod uplink_telemetry; #[path = "../../client/src/live_media_control.rs"] #[allow(warnings)] mod live_media_control; 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 relay_transport { pub fn endpoint(server_addr: &str) -> anyhow::Result { Ok(tonic::transport::Channel::from_shared( server_addr.to_string(), )?) } } mod input { pub mod camera { pub 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 new_with_capture_profile( source: Option<&str>, cfg: Option, _profile: Option<(u32, u32, u32)>, ) -> anyhow::Result { Self::new(source, cfg) } 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 new_default_source() -> anyhow::Result { Self::new() } pub fn new_with_source(_source: Option<&str>) -> anyhow::Result { Self::new() } pub fn pull(&self) -> Option { None } } } pub mod inputs { use lesavka_common::lesavka::{KeyboardReport, MouseReport}; use std::sync::{Arc, atomic::AtomicBool}; use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; pub struct InputAggregator { _kbd_tx: Sender, _mou_tx: Sender, _dev_mode: bool, _paste_tx: Option>, remote_capture_enabled: Arc, } 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, remote_capture_enabled: Arc::new(AtomicBool::new(true)), } } pub fn new_with_capture_mode( dev_mode: bool, kbd_tx: Sender, mou_tx: Sender, paste_tx: Option>, capture_remote_boot: bool, ) -> Self { let aggregator = Self::new(dev_mode, kbd_tx, mou_tx, paste_tx); aggregator .remote_capture_enabled .store(capture_remote_boot, std::sync::atomic::Ordering::Relaxed); aggregator } pub fn init(&mut self) -> anyhow::Result<()> { Ok(()) } pub async fn run(&mut self) -> anyhow::Result<()> { std::future::pending::<()>().await; #[allow(unreachable_code)] Ok(()) } pub fn remote_capture_enabled_handle(&self) -> Arc { Arc::clone(&self.remote_capture_enabled) } } } } mod output { pub mod audio { use lesavka_common::lesavka::AudioPacket; pub struct AudioOut; impl AudioOut { pub fn new() -> anyhow::Result { Ok(Self) } pub fn new_default_sink() -> anyhow::Result { Self::new() } pub fn new_with_sink(_sink: Option<&str>) -> anyhow::Result { Self::new() } 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; const DOWNLINK_MEDIA_SRC: &str = include_str!("../../client/src/app/downlink_media.rs"); const INPUT_STREAMS_SRC: &str = include_str!("../../client/src/app/input_streams.rs"); #[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 mut remote_capture_was_enabled = true; let pkt = keyboard_stream_report( Err(BroadcastStreamRecvError::Lagged(3)), true, &mut remote_capture_was_enabled, ) .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], }), true, &mut remote_capture_was_enabled, ) .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 mut remote_capture_was_enabled = true; let pkt = mouse_stream_report( Err(BroadcastStreamRecvError::Lagged(5)), true, &mut remote_capture_was_enabled, ) .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], }), true, &mut remote_capture_was_enabled, ) .expect("ok mouse item should pass through"); assert_eq!(passthrough.data, vec![9, 8, 7, 6]); } #[test] fn keyboard_stream_report_blocks_stale_packets_after_local_handoff() { let mut remote_capture_was_enabled = true; let reset = keyboard_stream_report( Ok(KeyboardReport { data: vec![4, 0, 5, 0, 0, 0, 0, 0], }), false, &mut remote_capture_was_enabled, ) .expect("switching local should emit one reset"); assert_eq!(reset.data, vec![0; 8]); let dropped = keyboard_stream_report( Ok(KeyboardReport { data: vec![1, 2, 3], }), false, &mut remote_capture_was_enabled, ); assert!( dropped.is_none(), "stale keyboard packets should be dropped locally" ); } #[test] fn mouse_stream_report_blocks_stale_packets_after_local_handoff() { let mut remote_capture_was_enabled = true; let reset = mouse_stream_report( Ok(MouseReport { data: vec![9, 8, 7, 6], }), false, &mut remote_capture_was_enabled, ) .expect("switching local should emit one neutral mouse packet"); assert_eq!(reset.data, vec![0; 4]); let dropped = mouse_stream_report( Ok(MouseReport { data: vec![1, 1, 1, 1], }), false, &mut remote_capture_was_enabled, ); assert!( dropped.is_none(), "stale mouse packets should be dropped locally" ); } #[test] fn audio_loop_backoff_contract_protects_server_from_reconnect_storms() { assert!(DOWNLINK_MEDIA_SRC.contains("let mut delay = Duration::from_secs(1);")); assert!(DOWNLINK_MEDIA_SRC.contains("tokio::time::sleep(delay).await;")); assert!(DOWNLINK_MEDIA_SRC.contains("delay = app_support::next_delay(delay);")); assert!(DOWNLINK_MEDIA_SRC.contains("consecutive_source_failures = 0;")); } #[test] fn input_streams_reconnect_quickly_then_back_off_under_outage() { assert!(INPUT_STREAMS_SRC.contains("INPUT_RECONNECT_BASE_DELAY")); assert!(INPUT_STREAMS_SRC.contains("Duration::from_millis(250)")); assert!(INPUT_STREAMS_SRC.contains("delay = app_support::next_delay(delay);")); assert!(INPUT_STREAMS_SRC.contains("tokio::time::sleep(delay).await;")); } }