#![allow(dead_code)] // Unit coverage for client audio recovery knobs. // // Scope: include the client recovery parser helpers without starting GTK, // GStreamer, or any relay process. // Targets: `client/src/app/audio_recovery_config.rs` and // `client/src/app/uplink_media/webcam_media_loop.rs`. // Why: the real-world Google Meet fix depends on one safe startup UAC epoch // retirement and bounded recovery retries, not on manual operator folklore. use std::time::{Duration, Instant}; use lesavka_common::lesavka::{KeyboardReport, MouseReport}; use serial_test::serial; use temp_env::{with_var, with_var_unset, with_vars}; use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use tracing::warn; include!("../../../../client/src/app/audio_recovery_config.rs"); const WEBCAM_MEDIA_LOOP_SRC: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/client/src/app/uplink_media/webcam_media_loop.rs" )); #[test] #[serial] fn remote_audio_usb_recovery_is_opt_in_and_cooldown_bounded() { with_var_unset("LESAVKA_AUDIO_AUTO_RECOVER_USB", || { assert!( !audio_usb_auto_recover_enabled(), "USB-level audio recovery must stay opt-in because hard gadget recovery can disturb a live call" ); }); with_vars( [ ("LESAVKA_AUDIO_AUTO_RECOVER_USB", Some("1")), ("LESAVKA_AUDIO_AUTO_RECOVER_AFTER", Some("0")), ("LESAVKA_AUDIO_AUTO_RECOVER_COOLDOWN_MS", Some("2500")), ], || { assert!(audio_usb_auto_recover_enabled()); assert_eq!( audio_usb_recover_after(), 3, "invalid thresholds should fall back to the safe default" ); assert_eq!(audio_usb_recover_cooldown(), Duration::from_millis(2500)); }, ); with_var( "LESAVKA_AUDIO_AUTO_RECOVER_COOLDOWN_MS", Some("not-a-number"), || { assert_eq!( audio_usb_recover_cooldown(), Duration::from_secs(60), "malformed cooldowns should not create a tight recovery loop" ); }, ); } #[test] fn remote_audio_error_classifier_targets_source_epoch_failures_only() { for message in [ "remote speaker capture produced no audio samples after 3000 ms on hw:UAC2Gadget,0", "remote speaker capture stalled for 3000 ms on hw:UAC2Gadget,0", "remote speaker capture cadence is too low on hw:UAC2Gadget,0", ] { assert!( is_recoverable_remote_audio_error(message), "expected recoverable remote audio source error: {message}" ); } for message in [ "TLS handshake failed", "recover_uac returned permission denied", "video decoder stalled", "microphone capture device missing locally", ] { assert!( !is_recoverable_remote_audio_error(message), "unrelated failures must not trigger USB/audio recovery: {message}" ); } } #[test] fn startup_epoch_auto_heal_is_one_shot_uac_recovery_after_three_seconds() { for marker in [ "const DEFAULT_UPSTREAM_AUTO_HEAL_AFTER_MS: u64 = 3_000;", "let mut startup_epoch_heal_delay = upstream_epoch_auto_heal_delay();", "startup_epoch_heal_delay.take()", "spawn_upstream_epoch_auto_heal(ep.clone(), heal_delay);", "client.recover_uac(Request::new(Empty {})).await", "automatic upstream A/V epoch heal requested", ] { assert!( WEBCAM_MEDIA_LOOP_SRC.contains(marker), "startup epoch auto-heal should preserve marker {marker}" ); } for forbidden in ["reset_usb(Request::new(Empty {})).await", "recover_uvc("] { assert!( !WEBCAM_MEDIA_LOOP_SRC.contains(forbidden), "startup epoch auto-heal must not affect USB or video recovery path: {forbidden}" ); } }