// crates/rutster-trunk/tests/reflex_on_trunk.rs // // T7 — Reflex-on-trunk-leg verification test (slice-5 spec §7). // // Proves that slice-4's `Reflex` + `LocalVadReflex` decorate the // trunk leg's `TapAudioPipe` identically to a WebRTC leg. A PSTN caller's loud // PCM triggers the same local-VAD state machine and the same `barge-in` kill // that a WebRTC caller's audio does. // // This is a *unit* integration test: it constructs the wrapped pipe stack // directly, without the binary's MediaThread or a real WSS brain. The heavier // end-to-end WSS sim lives in `sim_integ.rs` (T8). use std::sync::atomic::Ordering; use rutster_media::{ AudioSink, AudioSource, LocalVadReflex, PcmFrame, Reflex, ReflexMetrics, VAD_DEBOUNCE_FRAMES, }; use rutster_tap::TapAudioPipe; use tokio::sync::mpsc; /// Build a loud 24 kHz PCM frame whose RMS energy is well above /// `VAD_RMS_THRESHOLD`. The mock caller uses a constant amplitude of 1000, /// the same value slice-4's reflex unit tests use. fn loud_frame() -> PcmFrame { let mut frame = PcmFrame::zeroed(); for s in frame.samples.iter_mut() { *s = 1000; } frame } #[tokio::test] async fn local_vad_on_trunk_pipe_kills_playout_and_resumes_on_fresh_brain_audio() { // Given: a TapAudioPipe + the same Reflex composition the // FOB builds for every leg (WebRTC or trunk). let (tx_pcm_in, rx_pcm_in) = mpsc::channel(32); let (tx_audio_out, rx_audio_out) = mpsc::channel(32); let _rx_pcm_in = rx_pcm_in; // brain side; not used in this unit test let tap_metrics = rutster_tap::TapMetrics::new(); let tap_pipe = TapAudioPipe::new(tx_pcm_in, rx_audio_out, tap_metrics); let (advisory_tx, advisory_rx) = mpsc::channel(16); let metrics = ReflexMetrics::new(); let reflex = Reflex::new(tap_pipe, advisory_rx, metrics.clone()); let mut wrapped_pipe = LocalVadReflex::new(reflex, advisory_tx); // Pre-load one brain audio_out frame so the playout ring is non-empty. tx_audio_out.send(PcmFrame::zeroed()).await.unwrap(); // When: the PSTN caller speaks for `VAD_DEBOUNCE_FRAMES` consecutive ticks. let loud = loud_frame(); for _ in 0..VAD_DEBOUNCE_FRAMES { wrapped_pipe.on_pcm_frame(loud.clone()); } // Then: the third `next_pcm_frame` drains the local-VAD advisory, mutes the // pipe, flushes the ring, and returns `None` (playout is killed). let killed = wrapped_pipe.next_pcm_frame(); assert!( killed.is_none(), "local VAD must barge-in and suppress playout on the trunk leg" ); assert_eq!( metrics.barge_in_count.load(Ordering::Relaxed), 1, "barge-in must fire exactly once for the first loud utterance" ); // And when: a fresh brain reply arrives after the barge. tx_audio_out.send(PcmFrame::zeroed()).await.unwrap(); // Then: the Reflex un-mutes and returns the fresh frame. let resumed = wrapped_pipe.next_pcm_frame(); assert!( resumed.is_some(), "first fresh audio_out post-barge must resume trunk-leg playout" ); }