slice-4 (dev-a): MediaThread + session_map rewire (Task 6 + 7) #12
61
crates/rutster/tests/barge_in_integration.rs
Normal file
61
crates/rutster/tests/barge_in_integration.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// crates/rutster/tests/barge_in_integration.rs
|
||||||
|
//
|
||||||
|
// PRIMARY-path barge-in e2e proof (slice-4 spec §3.4, §6.1, §7 done-criteria #8).
|
||||||
|
//
|
||||||
|
// Constructs LocalVadReflex<Reflex<TapAudioPipe>> directly and proves that loud
|
||||||
|
// caller audio kills playout within one tick, with only the local VAD as the
|
||||||
|
// trigger source — no brain advisory is involved. This is wedge #1 from
|
||||||
|
// README:98-100 and ARCHITECTURE.md:79-81.
|
||||||
|
|
||||||
|
use rutster_media::{AudioSink, AudioSource, PcmFrame, VAD_DEBOUNCE_FRAMES};
|
||||||
|
use rutster_media::{LocalVadReflex, Reflex, ReflexMetrics};
|
||||||
|
use rutster_tap::{TapAudioPipe, TapMetrics};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn primary_path_local_vad_kills_playout_without_brain() {
|
||||||
|
let (tx_pcm_in, _rx_pcm_in) = mpsc::channel(32);
|
||||||
|
let (tx_audio_out, rx_audio_out) = mpsc::channel(32);
|
||||||
|
let tap_metrics = TapMetrics::new();
|
||||||
|
let pipe = TapAudioPipe::new(tx_pcm_in, rx_audio_out, tap_metrics);
|
||||||
|
|
||||||
|
let (advisory_tx, advisory_rx) = mpsc::channel(16);
|
||||||
|
let reflex_metrics = ReflexMetrics::new();
|
||||||
|
let reflex = Reflex::new(pipe, advisory_rx, reflex_metrics.clone());
|
||||||
|
let mut stack = LocalVadReflex::new(reflex, advisory_tx);
|
||||||
|
|
||||||
|
// Pre-load a brain audio_out frame into the ring (drain via next_pcm_frame).
|
||||||
|
tx_audio_out.send(PcmFrame::zeroed()).await.unwrap();
|
||||||
|
let _ = stack.next_pcm_frame(); // drain into ring
|
||||||
|
stack.next_pcm_frame(); // pop one (brain is playing)
|
||||||
|
|
||||||
|
// Loud caller audio × N → local VAD trips.
|
||||||
|
let mut loud = PcmFrame::zeroed();
|
||||||
|
for s in loud.samples.iter_mut() {
|
||||||
|
*s = 1000;
|
||||||
|
}
|
||||||
|
for _ in 0..VAD_DEBOUNCE_FRAMES {
|
||||||
|
stack.on_pcm_frame(loud.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next playout tick: kill applied, None returned (ring cleared).
|
||||||
|
let f = stack.next_pcm_frame();
|
||||||
|
assert!(
|
||||||
|
f.is_none(),
|
||||||
|
"primary-path local VAD must kill playout within 1 tick"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
reflex_metrics
|
||||||
|
.barge_in_count
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fresh brain audio_out → resume.
|
||||||
|
tx_audio_out.send(PcmFrame::zeroed()).await.unwrap();
|
||||||
|
let resumed = stack.next_pcm_frame();
|
||||||
|
assert!(
|
||||||
|
resumed.is_some(),
|
||||||
|
"first fresh audio_out post-barge must resume playout"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user