Files
rutster/crates/rutster-brain-realtime/tests/brain_mock_round_trip.rs
A.D.Lee c30a45232d
All checks were successful
CI / fmt (push) Successful in 1m40s
CI / clippy (push) Successful in 2m24s
CI / test (1.85) (push) Successful in 5m8s
CI / test (stable) (push) Successful in 5m20s
CI / deny (push) Successful in 1m34s
Slice 3 — OpenAI Realtime brain: swap echo for the brain (#4)
2026-07-01 22:25:09 +00:00

89 lines
4.2 KiB
Rust

//! slice-3 integration test: brain-process-level round-trip through the
//! MockRealtimeBrain + the translator + `run_openai_pump`. Drives the brain
//! the way the core's TapClient would (synthetic tap frames in, observes tap
//! frames out) — proves the dev-loop interop contract the PM directive's
//! Task 4 calls out ("actually start + interop on a basic audio round-trip")
//! without requiring a real WebRTC peer (browser-driven, same constraint as
//! slice-1/2).
//!
//! What this exercises end-to-end:
//! 1. MockRealtimeBrain accepts a WS connection + asserts session.update
//! has turn_detection: null (S4 — spec §4.3).
//! 2. `run_openai_pump` sends session.update + bridges tap frames ↔ OpenAI
//! events via the translator.
//! 3. A tap `audio_in` frames the test pushes through `tap_via` → translator
//! formats as `input_audio_buffer.append` → mock generates a canned
//! `response.audio.delta` → translator formats as tap `audio_out` →
//! arrives on `pump_tap_tx` (the pump's outbound mpsc).
use rutster_brain_realtime::{MockRealtimeBrain, openai_client::run_openai_pump};
use rutster_tap::{PcmFrame, decode_envelope, encode_audio_in, encode_hello};
use serde_json::Value;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
#[tokio::test]
async fn brain_mock_round_trip_translates_audio_in_to_audio_out() {
let mock = MockRealtimeBrain::start().await.unwrap();
let url = mock.url();
let req = url.as_str().into_client_request().unwrap();
let (openai_ws, _resp) = tokio_tungstenite::connect_async(req).await.unwrap();
// Bridge channels matching `run_openai_pump`'s signature (it owns neither
// side of the tap WS — the binary's main.rs does the WS forwarding; here
// we drive the pump directly with mpsc as the test's tap peer).
let (tap_via, pump_tap_rx) = mpsc::channel::<String>(64);
let (pump_tap_tx, mut tap_out_rx) = mpsc::channel::<String>(64);
// The pump sends session.update + then runs the select! loop. Spawn it.
let pump = tokio::spawn(async move {
run_openai_pump(openai_ws, pump_tap_rx, pump_tap_tx, "alloy".to_string()).await
});
// Push a hello (the pump ignores hello per its `_ => warn!(...)` arm,
// but real tap traffic always starts with hello).
let hello = encode_hello("test-session", 0, 0).unwrap();
tap_via.send(hello).await.unwrap();
// Push one audio_in frame. The pump translates to
// input_audio_buffer.append; the mock replies with a canned
// response.audio.delta; the pump translates back to a tap audio_out
// frame on pump_tap_tx.
let frame = PcmFrame::zeroed();
let audio_in = encode_audio_in(&frame, 1, 100).unwrap();
tap_via.send(audio_in).await.unwrap();
// Bounded-wait the audio_out frame coming back. Generous bound: the
// pump's tokio select! cycle + the mock's WS round-trip on loopback
// are sub-millisecond; 500 ms is fail-fast for any real regression.
let out_str = tokio::time::timeout(Duration::from_millis(500), tap_out_rx.recv())
.await
.expect("audio_out within 500ms")
.expect("channel not closed");
let decoded = decode_envelope(&out_str).unwrap();
match decoded.payload {
rutster_tap::DecodedPayload::AudioOut(p) => {
// The mock's canned PCM is 480 zeroed samples; the brain's
// translator round-trips it through PcmFrame (decode +
// re-encode), so the wire shape is exactly slice-2's audio_out.
assert_eq!(p.samples, 480);
let recovered = rutster_tap::decode_pcm(&p.pcm, p.samples).unwrap();
assert_eq!(recovered.samples.len(), 480);
// Canned zeros — every sample is 0.
assert_eq!(recovered.samples[0], 0);
assert_eq!(recovered.samples[479], 0);
// seq + ts carried through from the translator's per-event
// counter (initial value 0 on the first outbound frame).
assert_eq!(decoded.seq, 0);
}
other => panic!("expected AudioOut, got {other:?}"),
}
// Drop the input channel to signal the pump to exit cleanly.
drop(tap_via);
let _ = pump.await;
let _ = Value::Null; // suppress unused-import if Value unused here
}