diff --git a/codex-rs/core/tests/common/exec_server.rs b/codex-rs/core/tests/common/exec_server.rs new file mode 100644 index 0000000000..aa36d1a4d2 --- /dev/null +++ b/codex-rs/core/tests/common/exec_server.rs @@ -0,0 +1,73 @@ +//! Shared native exec-server process fixture; dropping it stops the child process. + +use anyhow::Context; +use anyhow::Result; +use anyhow::bail; +use std::process::Stdio; +use std::time::Duration; +use tempfile::TempDir; +use tokio::io::AsyncBufReadExt; +use tokio::io::BufReader; +use tokio::process::Child; +use tokio::process::ChildStdout; +use tokio::process::Command; +use tokio::time::Instant; +use tokio::time::timeout; + +const EXEC_SERVER_START_TIMEOUT: Duration = Duration::from_secs(30); + +pub struct ExecServerProcess { + _codex_home: TempDir, + child: Child, + _stdout: BufReader, + pub websocket_url: String, +} + +impl ExecServerProcess { + pub async fn start() -> Result { + let codex_home = TempDir::new()?; + let mut child = Command::new(codex_utils_cargo_bin::cargo_bin("codex")?) + .args(["exec-server", "--listen", "ws://127.0.0.1:0"]) + .env("CODEX_HOME", codex_home.path()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .kill_on_drop(true) + .spawn()?; + let stdout = child + .stdout + .take() + .context("exec-server stdout should be piped")?; + let mut stdout = BufReader::new(stdout); + let deadline = Instant::now() + EXEC_SERVER_START_TIMEOUT; + let websocket_url = loop { + let remaining = deadline + .checked_duration_since(Instant::now()) + .context("timed out waiting for exec-server listen URL")?; + let mut line = String::new(); + let bytes_read = timeout(remaining, stdout.read_line(&mut line)) + .await + .context("timed out reading exec-server listen URL")??; + if bytes_read == 0 { + bail!("exec-server exited before printing its listen URL"); + } + let line = line.trim(); + if line.starts_with("ws://") { + break line.to_string(); + } + }; + + Ok(Self { + _codex_home: codex_home, + child, + _stdout: stdout, + websocket_url, + }) + } +} + +impl Drop for ExecServerProcess { + fn drop(&mut self) { + let _ = self.child.start_kill(); + } +} diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 1f76febcee..2fe2b7de16 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -26,6 +26,7 @@ use std::path::PathBuf; pub mod apps_test_server; pub mod context_snapshot; +pub mod exec_server; pub mod hooks; pub mod process; pub mod responses; diff --git a/codex-rs/core/tests/suite/daybreak_access.rs b/codex-rs/core/tests/suite/daybreak_access.rs index 34cf689a97..4ae714c635 100644 --- a/codex-rs/core/tests/suite/daybreak_access.rs +++ b/codex-rs/core/tests/suite/daybreak_access.rs @@ -59,7 +59,7 @@ async fn daybreak_metadata_follows_the_actual_plugin_call(case: AccessCase) -> R skip_if_no_network!(Ok(())); #[cfg(unix)] let remote = if matches!(case, AccessCase::RemoteStdio) { - Some(super::multi_exec_server_sandbox::ExecServerProcess::start().await?) + Some(core_test_support::exec_server::ExecServerProcess::start().await?) } else { None }; diff --git a/codex-rs/core/tests/suite/multi_exec_server_sandbox.rs b/codex-rs/core/tests/suite/multi_exec_server_sandbox.rs index 2f9e3b942c..ad8515cf6e 100644 --- a/codex-rs/core/tests/suite/multi_exec_server_sandbox.rs +++ b/codex-rs/core/tests/suite/multi_exec_server_sandbox.rs @@ -1,11 +1,9 @@ use codex_core::TurnInputRequest; use codex_protocol::protocol::TurnEnvironmentRequests; -use std::process::Stdio; use std::time::Duration; use anyhow::Context; use anyhow::Result; -use anyhow::bail; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; @@ -15,6 +13,7 @@ use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::user_input::UserInput; use codex_utils_path_uri::PathUri; +use core_test_support::exec_server::ExecServerProcess; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_function_call; @@ -28,12 +27,6 @@ use core_test_support::wait_for_event_with_timeout; use pretty_assertions::assert_eq; use serde_json::json; use tempfile::TempDir; -use tokio::io::AsyncBufReadExt; -use tokio::io::BufReader; -use tokio::process::Child; -use tokio::process::ChildStdout; -use tokio::process::Command; -use tokio::time::Instant; use tokio::time::timeout; const FIRST_ENVIRONMENT_ID: &str = "first"; @@ -43,62 +36,6 @@ const SECOND_CALL_ID: &str = "write-from-second"; const EXEC_SERVER_START_TIMEOUT: Duration = Duration::from_secs(30); const TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30); -pub(super) struct ExecServerProcess { - _codex_home: TempDir, - child: Child, - _stdout: BufReader, - pub(super) websocket_url: String, -} - -impl ExecServerProcess { - pub(super) async fn start() -> Result { - let codex_home = TempDir::new()?; - let mut child = Command::new(codex_utils_cargo_bin::cargo_bin("codex")?) - .args(["exec-server", "--listen", "ws://127.0.0.1:0"]) - .env("CODEX_HOME", codex_home.path()) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) - .kill_on_drop(true) - .spawn()?; - let stdout = child - .stdout - .take() - .context("exec-server stdout should be piped")?; - let mut stdout = BufReader::new(stdout); - let deadline = Instant::now() + EXEC_SERVER_START_TIMEOUT; - let websocket_url = loop { - let remaining = deadline - .checked_duration_since(Instant::now()) - .context("timed out waiting for exec-server listen URL")?; - let mut line = String::new(); - let bytes_read = timeout(remaining, stdout.read_line(&mut line)) - .await - .context("timed out reading exec-server listen URL")??; - if bytes_read == 0 { - bail!("exec-server exited before printing its listen URL"); - } - let line = line.trim(); - if line.starts_with("ws://") { - break line.to_string(); - } - }; - - Ok(Self { - _codex_home: codex_home, - child, - _stdout: stdout, - websocket_url, - }) - } -} - -impl Drop for ExecServerProcess { - fn drop(&mut self) { - let _ = self.child.start_kill(); - } -} - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn two_exec_servers_isolate_workspace_write_roots() -> Result<()> { let first_exec_server = ExecServerProcess::start().await?; diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 5749351983..690ecc540b 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -674,7 +674,7 @@ printf 'STDIN_OK\n' }); // Exercise both native and executor-managed sandboxes with the same plugin fixture. let _executor = if environment_id == codex_exec_server::REMOTE_ENVIRONMENT_ID { - let executor = super::multi_exec_server_sandbox::ExecServerProcess::start().await?; + let executor = core_test_support::exec_server::ExecServerProcess::start().await?; builder = builder.with_exec_server_url(executor.websocket_url.clone()); Some(executor) } else { @@ -732,7 +732,7 @@ async fn remote_plugin_measurements_require_the_frontend_version() -> Result<()> skip_if_no_network!(Ok(())); // This fixture needs a real remote transport with separately controlled frontend // and executor caches, rather than the runner's automatic executor selection. - let executor = super::multi_exec_server_sandbox::ExecServerProcess::start().await?; + let executor = core_test_support::exec_server::ExecServerProcess::start().await?; let server = start_mock_server().await; let home = Arc::new(TempDir::new()?); let frontend_script = write_remote_plugin_script_and_config(home.as_ref()); diff --git a/codex-rs/core/tests/suite/turn_input_submission.rs b/codex-rs/core/tests/suite/turn_input_submission.rs index 920d864531..374fe229eb 100644 --- a/codex-rs/core/tests/suite/turn_input_submission.rs +++ b/codex-rs/core/tests/suite/turn_input_submission.rs @@ -1465,7 +1465,7 @@ async fn sampling_is_ready_for_daemon_recovery( .await; #[cfg(unix)] let remote = if executor == "remote" { - Some(super::multi_exec_server_sandbox::ExecServerProcess::start().await?) + Some(core_test_support::exec_server::ExecServerProcess::start().await?) } else { None };