Files
rutster/crates/rutster-trunk/tests/reflex_on_trunk.rs
Aaron D. Lee 0a76cec1df
Some checks failed
CI / clippy (pull_request) Successful in 2m25s
CI / test (1.85) (pull_request) Successful in 6m0s
CI / fmt (pull_request) Failing after 14m2s
CI / test (stable) (pull_request) Successful in 6m6s
CI / deny (pull_request) Failing after 1m32s
CI / twilio-live (manual only) (pull_request) Has been skipped
test(trunk): reflex-on-trunk verification (T7) + PSTN sim e2e (T8) — slice-5
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>
2026-07-05 12:14:43 -04:00

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"
);
}