use std::io::ErrorKind; use std::io::Read; use std::io::Write; use std::process::Command; use std::process::Stdio; use std::sync::mpsc; use std::thread; use std::time::Duration; use std::time::Instant; use anyhow::Context; use anyhow::anyhow; use pretty_assertions::assert_eq; #[cfg(unix)] use std::os::unix::net::UnixListener; #[cfg(windows)] use uds_windows::UnixListener; #[test] fn pipes_stdin_and_stdout_through_socket() -> anyhow::Result<()> { // This test intentionally avoids `read_to_end()` on the server side because // waiting for EOF can race with socket half-close behavior on slower runners. // Reading the exact request length keeps the test deterministic. // // We also use `std::process::Command` (instead of `assert_cmd`) so we can // poll/kill on timeout and include incremental server events + stderr in // failure output, which makes flaky failures actionable to debug. let dir = tempfile::TempDir::new().context("failed to create temp dir")?; let socket_path = dir.path().join("socket"); let request = b"request"; let request_path = dir.path().join("request.txt"); std::fs::write(&request_path, request).context("failed to write child stdin fixture")?; let listener = match UnixListener::bind(&socket_path) { Ok(listener) => listener, Err(err) if err.kind() == ErrorKind::PermissionDenied => { eprintln!("skipping test: failed to bind unix socket: {err}"); return Ok(()); } Err(err) => { return Err(err).context("failed to bind test unix socket"); } }; let (tx, rx) = mpsc::channel(); let (event_tx, event_rx) = mpsc::channel(); let server_thread = thread::spawn(move || -> anyhow::Result<()> { let _ = event_tx.send("waiting for accept".to_string()); let (mut connection, _) = listener .accept() .context("failed to accept test connection")?; let _ = event_tx.send("accepted connection".to_string()); let mut received = vec![0; request.len()]; connection .read_exact(&mut received) .context("failed to read data from client")?; let _ = event_tx.send(format!("read {} bytes", received.len())); tx.send(received) .map_err(|_| anyhow!("failed to send received bytes to test thread"))?; connection .write_all(b"response") .context("failed to write response to client")?; let _ = event_tx.send("wrote response".to_string()); Ok(()) }); let stdin = std::fs::File::open(&request_path).context("failed to open child stdin fixture")?; let mut child = Command::new(codex_utils_cargo_bin::cargo_bin("codex-stdio-to-uds")?) .arg(&socket_path) .stdin(Stdio::from(stdin)) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .context("failed to spawn codex-stdio-to-uds")?; let mut child_stdout = child.stdout.take().context("missing child stdout")?; let mut child_stderr = child.stderr.take().context("missing child stderr")?; let (stdout_tx, stdout_rx) = mpsc::channel(); let (stderr_tx, stderr_rx) = mpsc::channel(); thread::spawn(move || { let mut stdout = Vec::new(); let result = child_stdout.read_to_end(&mut stdout).map(|_| stdout); let _ = stdout_tx.send(result); }); thread::spawn(move || { let mut stderr = Vec::new(); let result = child_stderr.read_to_end(&mut stderr).map(|_| stderr); let _ = stderr_tx.send(result); }); let mut server_events = Vec::new(); let deadline = Instant::now() + Duration::from_secs(5); let status = loop { while let Ok(event) = event_rx.try_recv() { server_events.push(event); } if let Some(status) = child.try_wait().context("failed to poll child status")? { break status; } if Instant::now() >= deadline { let _ = child.kill(); let _ = child.wait(); let stderr = stderr_rx .recv_timeout(Duration::from_secs(1)) .context("timed out waiting for child stderr after kill")? .context("failed to read child stderr")?; anyhow::bail!( "codex-stdio-to-uds did not exit in time; server events: {:?}; stderr: {}", server_events, String::from_utf8_lossy(&stderr).trim_end() ); } thread::sleep(Duration::from_millis(25)); }; let stdout = stdout_rx .recv_timeout(Duration::from_secs(1)) .context("timed out waiting for child stdout")? .context("failed to read child stdout")?; let stderr = stderr_rx .recv_timeout(Duration::from_secs(1)) .context("timed out waiting for child stderr")? .context("failed to read child stderr")?; assert!( status.success(), "codex-stdio-to-uds exited with {status}; server events: {:?}; stderr: {}", server_events, String::from_utf8_lossy(&stderr).trim_end() ); assert_eq!(stdout, b"response"); let received = rx .recv_timeout(Duration::from_secs(1)) .context("server did not receive data in time")?; assert_eq!(received, request); let server_result = server_thread .join() .map_err(|_| anyhow!("server thread panicked"))?; server_result.context("server failed")?; Ok(()) }