slice-4 (dev-a): Task 9 Step 1 — primary-path barge-in e2e (wedge #1) #10
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