test: require explicit mic source for mirrored probe
This commit is contained in:
parent
c0d61bb87f
commit
53bca123d9
18
AGENTS.md
18
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.
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.17.17"
|
||||
version = "0.17.18"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.17.17"
|
||||
version = "0.17.18"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.17.17"
|
||||
version = "0.17.18"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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}\"",
|
||||
|
||||
@ -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}"
|
||||
),
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user