test: require explicit mic source for mirrored probe

This commit is contained in:
Brad Stein 2026-05-02 15:08:31 -03:00
parent c0d61bb87f
commit 53bca123d9
9 changed files with 74 additions and 7 deletions

View File

@ -375,3 +375,21 @@ Context: 0.17.16 successfully installed and token-verified the first browser cap
- [x] Update manual probe contract tests for analyzer-failure continuation.
- [x] Run shell syntax checks, focused contract tests, and package checks.
- [x] Push clean semver `0.17.17` for installed client/server testing.
## 0.17.18 Explicit Probe Source Integrity Checklist
Context: the 0.17.17 Bumblebee mirrored run was configured with
`LESAVKA_MIC_SOURCE=alsa_input.usb-Neat_Microphones_Bumblebee_II_USB_Microphone-00.mono-fallback`,
but the Bumblebee was unplugged. The client log recorded `requested mic ... not found; using default`,
so the run measured the fallback/default microphone and must not be treated as Bumblebee calibration
evidence.
- [x] Treat 0.17.17 Bumblebee probe metrics as invalid for Bumblebee-specific calibration.
- [x] Keep ordinary launcher/client source selection forgiving by default.
- [x] Add strict explicit-source mode with `LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES=1`.
- [x] In strict mode, fail client startup when requested `LESAVKA_MIC_SOURCE` is unavailable instead of falling back to default.
- [x] Make the mirrored manual probe launch the real client with strict explicit-source mode by default.
- [x] Add contract coverage so the mirrored probe cannot regress to silent explicit-source fallback.
- [x] Run shell syntax checks, focused contract tests, and package checks.
- [x] Push clean semver `0.17.18` for installed client/server testing.
- [ ] Re-run the mirrored probe only after confirming the intended microphone is physically present and selected.

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.17.17"
version = "0.17.18"
dependencies = [
"anyhow",
"async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.17.17"
version = "0.17.18"
dependencies = [
"anyhow",
"base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.17.17"
version = "0.17.18"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.17.17"
version = "0.17.18"
edition = "2024"
[dependencies]

View File

@ -1,5 +1,5 @@
// client/src/input/microphone.rs
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use gst::prelude::*;
use gstreamer as gst;
use gstreamer_app as gst_app;
@ -27,6 +27,7 @@ const MIC_LEVEL_TAP_ENV: &str = "LESAVKA_UPLINK_MIC_LEVEL";
const MIC_PULSE_BUFFER_TIME_ENV: &str = "LESAVKA_MIC_PULSE_BUFFER_TIME_US";
const MIC_PULSE_LATENCY_TIME_ENV: &str = "LESAVKA_MIC_PULSE_LATENCY_TIME_US";
const MIC_PACKET_TARGET_DURATION_ENV: &str = "LESAVKA_MIC_PACKET_TARGET_US";
const REQUIRE_EXPLICIT_MEDIA_SOURCES_ENV: &str = "LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES";
const MIC_SAMPLE_RATE: u64 = 48_000;
const MIC_CHANNELS: usize = 2;
const MIC_SAMPLE_BYTES: usize = std::mem::size_of::<i16>();
@ -75,6 +76,11 @@ impl MicrophoneCapture {
Some(s) if !s.is_empty() => match Self::resolve_source_desc(&s) {
Some(desc) => desc,
None => {
if explicit_media_sources_required() {
bail!(
"requested mic '{s}' was not found; refusing to use default because {REQUIRE_EXPLICIT_MEDIA_SOURCES_ENV}=1"
);
}
warn!("🎤 requested mic '{s}' not found; using default");
Self::default_source_desc()
}
@ -452,6 +458,20 @@ fn positive_u64_env(name: &str, default_value: u64) -> u64 {
.unwrap_or(default_value)
}
fn explicit_media_sources_required() -> bool {
bool_env_enabled(REQUIRE_EXPLICIT_MEDIA_SOURCES_ENV)
}
fn bool_env_enabled(name: &str) -> bool {
std::env::var(name).ok().is_some_and(|value| {
let value = value.trim();
value == "1"
|| value.eq_ignore_ascii_case("true")
|| value.eq_ignore_ascii_case("yes")
|| value.eq_ignore_ascii_case("on")
})
}
/// Detect launcher catalog names that should be opened through Pulse directly.
fn looks_like_pulse_source_name(source: &str) -> bool {
let source = source.trim();

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.17.17"
version = "0.17.18"
edition = "2024"
build = "build.rs"

View File

@ -425,6 +425,7 @@ start_real_lesavka_client() {
LESAVKA_SERVER_ADDR="${RESOLVED_LESAVKA_SERVER_ADDR}" \
LESAVKA_TLS_DOMAIN="${LESAVKA_TLS_DOMAIN}" \
LESAVKA_MEDIA_CONTROL="${MEDIA_CONTROL}" \
LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES="${LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES:-1}" \
LESAVKA_UPSTREAM_TIMING_TRACE="${LESAVKA_UPSTREAM_TIMING_TRACE:-1}" \
RUST_LOG="${RUST_LOG:-warn,lesavka_client::app=info,lesavka_client::input::camera=info,lesavka_client::input::microphone=info}" \
"${REPO_ROOT}/target/debug/lesavka-client" --no-launcher --server "${RESOLVED_LESAVKA_SERVER_ADDR}"

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.17.17"
version = "0.17.18"
edition = "2024"
autobins = false

View File

@ -103,6 +103,7 @@ fn mirrored_sync_script_uses_real_client_capture_path() {
"lesavka-client",
"LESAVKA_HEADLESS=1",
"LESAVKA_MEDIA_CONTROL=\"${MEDIA_CONTROL}\"",
"LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES=\"${LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES:-1}\"",
"--no-launcher --server \"${RESOLVED_LESAVKA_SERVER_ADDR}\"",
"BROWSER_SYNC_DRIVER_COMMAND=\"${driver_command}\"",
"SYNC_ANALYZE_EVENT_WIDTH_CODES=\"${PROBE_EVENT_WIDTH_CODES}\"",

View File

@ -510,4 +510,31 @@ exit 0
});
});
}
#[test]
#[serial]
fn strict_probe_mode_rejects_missing_requested_source() {
let script = r#"#!/usr/bin/env sh
if [ "$1" = "list" ] && [ "$2" = "short" ] && [ "$3" = "sources" ]; then
echo "0 alsa_input.pci.monitor module-alsa-card.c s16le 2ch 48000Hz RUNNING"
echo "1 alsa_input.usb-DeskMic_777-00.analog-stereo module-alsa-card.c s16le 2ch 48000Hz IDLE"
exit 0
fi
exit 0
"#;
with_fake_pactl(script, || {
with_var("LESAVKA_MIC_SOURCE", Some("missing-fragment"), || {
with_var("LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES", Some("1"), || {
match MicrophoneCapture::new() {
Ok(_) => panic!("missing mic should fail"),
Err(err) => assert!(
err.to_string()
.contains("requested mic 'missing-fragment' was not found"),
"unexpected error: {err}"
),
}
});
});
});
}
}