test: fail probe on required source setup errors

This commit is contained in:
Brad Stein 2026-05-02 15:27:50 -03:00
parent 53bca123d9
commit 160cbffbd4
7 changed files with 86 additions and 6 deletions

View File

@ -393,3 +393,19 @@ evidence.
- [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.
## 0.17.19 Fatal Required Source Failure Checklist
Context: the 0.17.18 run proved fallback was blocked, but the headless client kept running
camera-only after the required Bumblebee source failed to open. The server stayed in `acquiring`
for all four segments (`awaiting both upstream media streams`), the analyzer saw no color-coded
video pulses, and no calibration data was produced. Required-source failure must fail the probe,
not degrade into camera-only evidence.
- [x] Treat the 0.17.18 run as a required-microphone setup failure, not a lip-sync measurement.
- [x] Keep strict no-fallback behavior from 0.17.18.
- [x] Abort the client process when an explicit required microphone source cannot start.
- [x] Abort the client process when an explicit required camera source cannot start.
- [x] Run shell syntax checks, focused contract tests, and package checks.
- [x] Push clean semver `0.17.19` for installed client/server testing.
- [ ] Re-run only after `LESAVKA_MIC_SOURCE` is listed by the local audio stack.

6
Cargo.lock generated
View File

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

View File

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

View File

@ -40,6 +40,7 @@ impl LesavkaClientApp {
"🎤 microphone uplink setup failed for {:?}: {err:#}",
active_source.as_deref().unwrap_or("auto")
);
abort_if_required_media_source_failed("microphone", "🎤", active_source.as_deref(), &err);
delay = app_support::next_delay(delay);
tokio::time::sleep(delay).await;
continue;
@ -47,6 +48,12 @@ impl LesavkaClientApp {
Err(err) => {
telemetry.record_disconnect(format!("microphone uplink setup task failed: {err}"));
warn!("🎤 microphone uplink setup task failed before StreamMicrophone could start: {err}");
abort_if_required_media_source_failed(
"microphone",
"🎤",
active_source.as_deref(),
&err,
);
delay = app_support::next_delay(delay);
tokio::time::sleep(delay).await;
continue;
@ -216,6 +223,7 @@ impl LesavkaClientApp {
"📸 webcam uplink setup failed for {:?}: {err:#}",
active_source.as_deref().unwrap_or("auto")
);
abort_if_required_media_source_failed("camera", "📸", active_source.as_deref(), &err);
delay = app_support::next_delay(delay);
tokio::time::sleep(delay).await;
continue;
@ -223,6 +231,12 @@ impl LesavkaClientApp {
Err(err) => {
telemetry.record_disconnect(format!("webcam uplink setup task failed: {err}"));
warn!("📸 webcam uplink setup task failed before StreamCamera could start: {err}");
abort_if_required_media_source_failed(
"camera",
"📸",
active_source.as_deref(),
&err,
);
delay = app_support::next_delay(delay);
tokio::time::sleep(delay).await;
continue;
@ -412,6 +426,39 @@ fn parse_camera_profile_id(raw: &str) -> Option<(u32, u32, u32)> {
(width > 0 && height > 0 && fps > 0).then_some((width, height, fps))
}
#[cfg(not(coverage))]
fn abort_if_required_media_source_failed(
kind: &str,
icon: &str,
source: Option<&str>,
err: &dyn std::fmt::Display,
) {
if !explicit_media_sources_required() || source.is_none_or(|source| source.trim().is_empty()) {
return;
}
let source = source.expect("checked source presence");
error!(
"{icon} required {kind} source '{source}' failed to start; aborting client because LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES=1: {err}"
);
eprintln!(
"{icon} required {kind} source '{source}' failed to start; aborting client because LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES=1: {err}"
);
std::process::exit(2);
}
#[cfg(not(coverage))]
fn explicit_media_sources_required() -> bool {
std::env::var("LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES")
.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")
})
}
#[cfg(not(coverage))]
const VIDEO_UPLINK_QUEUE: crate::uplink_fresh_queue::FreshQueueConfig =
crate::uplink_fresh_queue::FreshQueueConfig {

View File

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

View File

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

View File

@ -121,3 +121,20 @@ fn sync_probe_audio_queue_preserves_bounded_marker_continuity() {
);
assert_queue_policy(block, "PROBE_AUDIO_QUEUE", "DrainOldest");
}
#[test]
fn strict_explicit_media_source_failures_abort_the_headless_probe_client() {
for expected in [
"LESAVKA_REQUIRE_EXPLICIT_MEDIA_SOURCES",
"abort_if_required_media_source_failed",
"required {kind} source '{source}' failed to start",
"std::process::exit(2)",
"abort_if_required_media_source_failed(\"microphone\"",
"abort_if_required_media_source_failed(\"camera\"",
] {
assert!(
UPLINK_MEDIA_SRC.contains(expected),
"required-source setup failures must be fatal in strict probe mode: missing {expected}"
);
}
}