diff --git a/testing/tests/server_runtime_smoke_contract.rs b/testing/tests/server_runtime_smoke_contract.rs index c5b2fc2..1ef6849 100644 --- a/testing/tests/server_runtime_smoke_contract.rs +++ b/testing/tests/server_runtime_smoke_contract.rs @@ -7,7 +7,7 @@ //! Why: these contracts provide broad safety coverage around startup/recovery //! code paths that are otherwise hard to hit from unit-only tests. -use lesavka_server::audio::ClipTap; +use lesavka_server::audio::{self, ClipTap}; use lesavka_server::gadget::UsbGadget; use lesavka_server::runtime_support; use serial_test::serial; @@ -61,6 +61,47 @@ fn clip_tap_writes_rotating_file_when_period_elapses() { } } +#[test] +#[serial] +fn clip_tap_drop_flushes_pending_audio() { + let tag = "lesavka-cliptap-drop-contract"; + let prefix = format!("{tag}-"); + let before = tmp_files_with_prefix(&prefix); + + let mut tap = ClipTap::new(tag, Duration::from_secs(60)); + tap.feed(b"pending-bytes"); + drop(tap); + + let after = tmp_files_with_prefix(&prefix); + let created: Vec = after.difference(&before).cloned().collect(); + assert!( + !created.is_empty(), + "expected drop() to flush buffered clip bytes" + ); + for path in created { + let _ = std::fs::remove_file(path); + } +} + +#[test] +#[serial] +fn clip_tap_flush_is_noop_when_buffer_is_empty() { + let tag = "lesavka-cliptap-empty-contract"; + let prefix = format!("{tag}-"); + let before = tmp_files_with_prefix(&prefix); + + let mut tap = ClipTap::new(tag, Duration::from_secs(60)); + tap.flush(); + drop(tap); + + let after = tmp_files_with_prefix(&prefix); + let created: Vec = after.difference(&before).cloned().collect(); + assert!( + created.is_empty(), + "empty flush/drop should not create clip artifacts" + ); +} + #[test] #[serial] fn gadget_queries_handle_missing_controller_paths() { @@ -108,6 +149,25 @@ fn runtime_open_voice_with_retry_stays_bounded_for_missing_device() { }); } +#[test] +#[serial] +fn runtime_ear_handles_malformed_alsa_source_without_hanging() { + let rt = Runtime::new().expect("create runtime"); + let outcome = rt.block_on(async { + tokio::time::timeout( + Duration::from_secs(3), + audio::ear("hw:UAC2Gadget,0\" ! malformed-pipeline", 0), + ) + .await + }); + + match outcome { + Ok(Ok(stream)) => drop(stream), + Ok(Err(err)) => assert!(!err.to_string().trim().is_empty()), + Err(_) => panic!("audio::ear timed out for malformed ALSA source"), + } +} + #[test] #[serial] fn runtime_recover_hid_ignores_non_transport_errors() {