diff --git a/AGENTS.md b/AGENTS.md index cb8af58..fa254eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 9ac5ba7..806e0a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/client/Cargo.toml b/client/Cargo.toml index c1c8696..3c793df 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.17.17" +version = "0.17.18" edition = "2024" [dependencies] diff --git a/client/src/input/microphone.rs b/client/src/input/microphone.rs index d2fd698..2ec640e 100644 --- a/client/src/input/microphone.rs +++ b/client/src/input/microphone.rs @@ -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::(); @@ -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(); diff --git a/common/Cargo.toml b/common/Cargo.toml index 55ca764..6ad1733 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.17.17" +version = "0.17.18" edition = "2024" build = "build.rs" diff --git a/scripts/manual/run_upstream_mirrored_av_sync.sh b/scripts/manual/run_upstream_mirrored_av_sync.sh index 799083c..f6fe215 100755 --- a/scripts/manual/run_upstream_mirrored_av_sync.sh +++ b/scripts/manual/run_upstream_mirrored_av_sync.sh @@ -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}" diff --git a/server/Cargo.toml b/server/Cargo.toml index 6a36a67..599ed57 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.17.17" +version = "0.17.18" edition = "2024" autobins = false diff --git a/testing/tests/client_manual_sync_script_contract.rs b/testing/tests/client_manual_sync_script_contract.rs index 0bf4528..c92dcd0 100644 --- a/testing/tests/client_manual_sync_script_contract.rs +++ b/testing/tests/client_manual_sync_script_contract.rs @@ -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}\"", diff --git a/testing/tests/client_microphone_include_contract.rs b/testing/tests/client_microphone_include_contract.rs index 49ea161..eeeb449 100644 --- a/testing/tests/client_microphone_include_contract.rs +++ b/testing/tests/client_microphone_include_contract.rs @@ -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}" + ), + } + }); + }); + }); + } }