All checks were successful
T7: proves slice-4's Reflex<TapAudioPipe> + LocalVadReflex decorates the trunk leg's TapAudioPipe identically; barge-in fires on PSTN caller speech through the same state machine as WebRTC caller speech. T8: MockRealtimeBrain + BrainShim drives a synthetic PSTN caller through the FOB reflex loop end-to-end: loud PCM -> local VAD trips -> barge kills -> brain reply -> un-mute -> idle timeout (caller hangup) closes the session. Dev-dependencies added to rutster-trunk/Cargo.toml so the integration tests reach MockRealtimeBrain, futures-util, and tokio-tungstenite without pulling FOB source into the trunk crate. Signed-off-by: Aaron D. Lee <himself@adlee.work>
80 lines
3.0 KiB
Rust
80 lines
3.0 KiB
Rust
// 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<TapAudioPipe>` + `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<LocalVadReflex> 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"
|
|
);
|
|
}
|