lesavka/tests/unit/client/app/client_audio_recovery_config_unit.rs

116 lines
3.9 KiB
Rust

#![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}"
);
}
}