Files
codex/codex-rs/realtime-webrtc/tests/common/mod.rs
Benjamin Carlsson 64e9a68987 Add a helper-backed realtime WebRTC session API (#43097)
## What changed

- Add `RealtimeWebrtcSession` and cloneable handles for startup, answer negotiation, audio controls, level meters, and error reporting.
- Open devices after answer negotiation and apply the latest controls before enabling audio. Preserve subsequent control transitions in order and close the session if the command queue fills.
- Cancel pending work on explicit close, external cancellation, or final handle drop, with a shared runtime keeping helper reaping alive.
- Add audio-state polling to consume microphone and speaker peaks and detect helper, device, or connection failures.
- Check packaged helper/runtime availability and initialize build information in the standalone TUI for the helper handshake.

## Testing

Add unit and helper-process integration tests covering startup control ordering, queue overflow, cancellation, helper reaping and loss, package availability, SDP debug redaction, and peak accumulation, clamping, and consumption.

GitOrigin-RevId: 458cd80353697f7411cee785f380d1d53aef8416
2026-09-05 20:39:46 +00:00

220 lines
7.5 KiB
Rust

//! Copies this test executable into a private package and dispatches its fake helper before libtest.
//! The fixture validates protocol order; it never loads native libraries or opens audio devices.
use anyhow::Context;
use anyhow::Result;
use anyhow::ensure;
use codex_realtime_webrtc::AudioState;
use codex_realtime_webrtc::Message;
use codex_realtime_webrtc::encode_frame;
use codex_realtime_webrtc::read_message;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
pub const BUILD_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567";
pub const WAIT: Duration = Duration::from_secs(/*secs*/ 10);
const APP: &str = if cfg!(windows) { "codex.exe" } else { "codex" };
const HELPER: &str = if cfg!(windows) {
"codex-voice-host.exe"
} else {
"codex-voice-host"
};
pub fn wait_for(mut ready: impl FnMut() -> bool) -> Result<()> {
let deadline = Instant::now() + WAIT;
while !ready() {
ensure!(Instant::now() < deadline, "fixture deadline expired");
thread::sleep(Duration::from_millis(/*millis*/ 5));
}
Ok(())
}
#[cfg(unix)]
pub fn wait_for_helper_reaped(root: &std::path::Path) -> Result<()> {
let pid: libc::pid_t = fs::read_to_string(root.join("helper-pid"))?.parse()?;
wait_for(|| {
// SAFETY: Signal zero checks process existence without delivering a signal.
let result = unsafe {
libc::kill(pid, /*sig*/ 0)
};
result == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
})
}
struct Package(PathBuf);
impl Drop for Package {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
// The child discovers its installation through current_exe, exactly as the public facade does.
pub fn package(test: &str) -> Result<Option<PathBuf>> {
let source = std::env::current_exe()?;
if source.file_name().is_some_and(|name| name == APP) {
codex_build_info::BuildInfo::initialize(BUILD_COMMIT);
return Ok(Some(
source
.parent()
.context("bin")?
.parent()
.context("package")?
.to_owned(),
));
}
let nonce = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
let package =
Package(std::env::temp_dir().join(format!("voice-actor-{}-{nonce}", std::process::id())));
let root = &package.0;
fs::create_dir_all(root.join("bin"))?;
fs::create_dir_all(root.join("codex-resources/voice/bin"))?;
fs::write(root.join("codex-package.json"), "{}")?;
fs::copy(&source, root.join("bin").join(APP))?;
fs::copy(&source, root.join("codex-resources/voice/bin").join(HELPER))?;
let mut child = Command::new(root.join("bin").join(APP))
.args(["--exact", test, "--nocapture"])
.current_dir(root)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let deadline = Instant::now() + WAIT * 3;
while child.try_wait()?.is_none() {
if Instant::now() >= deadline {
child.kill()?;
child.wait()?;
anyhow::bail!("packaged test timed out");
}
thread::sleep(Duration::from_millis(/*millis*/ 10));
}
let output = child.wait_with_output()?;
ensure!(
output.status.success(),
"packaged test failed: {}\n{}\nfixture: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
fs::read_to_string(root.join("fixture-error")).unwrap_or_default()
);
Ok(None)
}
#[ctor::ctor]
fn dispatch_helper() {
if std::env::current_exe()
.ok()
.and_then(|path| path.file_name().map(std::ffi::OsStr::to_owned))
.is_some_and(|name| name == HELPER)
{
if let Err(error) = helper() {
let _ = fs::write("fixture-error", format!("{error:#}"));
std::process::exit(1);
}
std::process::exit(0);
}
}
#[derive(PartialEq)]
enum Stage {
Hello,
Initialize,
Offer,
Answer,
Devices,
Controls,
Running,
}
fn helper() -> Result<()> {
let root = std::env::current_dir()?;
fs::write(root.join("helper-pid"), std::process::id().to_string())?;
let mut input = std::io::stdin().lock();
let mut output = std::io::stdout().lock();
let mut stage = Stage::Hello;
let mut controls_history = Vec::new();
while let Some(request) = read_message(&mut input)? {
let response = match request {
Message::Hello {
protocol: 1,
build_commit,
} if stage == Stage::Hello => {
ensure!(build_commit == BUILD_COMMIT, "wrong executable build stamp");
stage = Stage::Initialize;
Message::Ready {}
}
Message::InitializeRuntime {} if stage == Stage::Initialize => {
fs::write(root.join("initializing"), [])?;
if root.join("hold-initialization").exists() {
wait_for(|| root.join("release").exists())?;
}
stage = Stage::Offer;
Message::RuntimeReady {}
}
Message::StartTransport {} if stage == Stage::Offer => {
stage = Stage::Answer;
Message::Offer {
sdp: "synthetic-offer"
.to_owned()
.try_into()
.map_err(anyhow::Error::msg)?,
}
}
Message::ApplyAnswer { sdp } if stage == Stage::Answer => {
ensure!(
sdp.into_sdp() == "synthetic-answer",
"unexpected fixture answer"
);
fs::write(root.join("answer"), [])?;
wait_for(|| root.join("release").exists())?;
stage = Stage::Devices;
Message::TransportReady {}
}
Message::OpenDevices {} if stage == Stage::Devices => {
stage = Stage::Controls;
Message::DevicesOpened {}
}
Message::SetAudioControls { controls }
if stage == Stage::Controls || stage == Stage::Running =>
{
controls_history.push(controls);
fs::write(
root.join("controls"),
serde_json::to_vec(&controls_history)?,
)?;
stage = Stage::Running;
Message::AudioControlsApplied {}
}
Message::InspectAudio {} if stage == Stage::Answer || stage == Stage::Running => {
if root.join("exit").exists() {
return Ok(());
}
Message::AudioState {
state: if stage == Stage::Running {
AudioState {
microphone_peak: 123,
speaker_peak: 456,
}
} else {
AudioState::default()
},
}
}
Message::Close {} => {
output.write_all(&encode_frame(&Message::Closed {})?)?;
output.flush()?;
return Ok(());
}
_ => anyhow::bail!("unexpected fixture protocol sequence"),
};
output.write_all(&encode_frame(&response)?)?;
output.flush()?;
}
Ok(())
}