diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 947111d3fc..8768290ce7 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -55,7 +55,6 @@ use crate::responses::WebSocketTestServer; use crate::responses::output_value_to_text; use crate::responses::start_mock_server; use crate::streaming_sse::StreamingSseServer; -use crate::wait_for_event_match; use crate::wait_for_event_with_timeout; use wiremock::Match; use wiremock::matchers::path_regex; @@ -65,6 +64,9 @@ type PreBuildHook = dyn FnOnce(&Path) + Send + 'static; type WorkspaceSetup = dyn FnOnce(AbsolutePathBuf, Arc) -> BoxFuture<'static, Result<()>> + Send; const TEST_MODEL_WITH_EXPERIMENTAL_TOOLS: &str = "test-gpt-5.1-codex"; +const REMOTE_CODEX_PATH_ENV_VAR: &str = "CODEX_TEST_REMOTE_CODEX_PATH"; +const DEFAULT_REMOTE_CODEX_PATH: &str = "/tmp/codex-remote-env/codex"; +const REMOTE_CODEX_LINUX_SANDBOX_BASENAME: &str = "codex-linux-sandbox"; const REMOTE_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_TEST_REMOTE_EXEC_SERVER_URL"; static REMOTE_TEST_INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0); const SUBMIT_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30); @@ -73,6 +75,8 @@ const SUBMIT_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30); pub struct TestEnv { environment: codex_exec_server::Environment, exec_server_url: Option, + remote_codex_path: Option, + remote_codex_linux_sandbox_exe: Option, cwd: AbsolutePathBuf, local_cwd_temp_dir: Option>, remote_container_name: Option, @@ -87,6 +91,8 @@ impl TestEnv { Ok(Self { environment, exec_server_url: None, + remote_codex_path: None, + remote_codex_linux_sandbox_exe: None, cwd, local_cwd_temp_dir: Some(local_cwd_temp_dir), remote_container_name: None, @@ -133,6 +139,8 @@ pub async fn test_env() -> Result { Ok(TestEnv { environment, exec_server_url: Some(websocket_url), + remote_codex_path: Some(remote_codex_path()?), + remote_codex_linux_sandbox_exe: Some(remote_codex_linux_sandbox_exe()?), cwd, local_cwd_temp_dir: None, remote_container_name: Some(remote_env.container_name), @@ -163,6 +171,24 @@ fn remote_exec_server_url() -> Result { Ok(listen_url.to_string()) } +fn remote_codex_path() -> Result { + let codex_path = std::env::var(REMOTE_CODEX_PATH_ENV_VAR) + .unwrap_or_else(|_| DEFAULT_REMOTE_CODEX_PATH.to_string()); + let codex_path = codex_path.trim(); + if codex_path.is_empty() { + return Err(anyhow!("{REMOTE_CODEX_PATH_ENV_VAR} must not be empty")); + } + Ok(PathBuf::from(codex_path)) +} + +fn remote_codex_linux_sandbox_exe() -> Result { + let remote_codex_path = remote_codex_path()?; + let remote_codex_dir = remote_codex_path + .parent() + .ok_or_else(|| anyhow!("{REMOTE_CODEX_PATH_ENV_VAR} must have a parent directory"))?; + Ok(remote_codex_dir.join(REMOTE_CODEX_LINUX_SANDBOX_BASENAME)) +} + fn remote_test_instance_id() -> String { let instance = REMOTE_TEST_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); format!("{}-{instance}", std::process::id()) @@ -406,9 +432,15 @@ impl TestCodexBuilder { test_env: TestEnv, include_local_environment: bool, ) -> anyhow::Result { - let (config, fallback_cwd) = self + let (mut config, fallback_cwd) = self .prepare_config(base_url, &home, test_env.cwd().clone()) .await?; + if let Some(remote_codex_path) = &test_env.remote_codex_path { + config.codex_self_exe = Some(remote_codex_path.clone()); + } + if let Some(remote_codex_linux_sandbox_exe) = &test_env.remote_codex_linux_sandbox_exe { + config.codex_linux_sandbox_exe = Some(remote_codex_linux_sandbox_exe.clone()); + } let exec_server_url = self .exec_server_url .clone() @@ -783,11 +815,16 @@ impl TestCodex { }) .await?; - let turn_id = wait_for_event_match(&self.codex, |event| match event { - EventMsg::TurnStarted(event) => Some(event.turn_id.clone()), - _ => None, - }) - .await; + let turn_id = match wait_for_event_with_timeout( + &self.codex, + |event| matches!(event, EventMsg::TurnStarted(_)), + SUBMIT_TURN_COMPLETE_TIMEOUT, + ) + .await + { + EventMsg::TurnStarted(event) => event.turn_id, + _ => unreachable!("predicate only matches TurnStarted"), + }; wait_for_event_with_timeout( &self.codex, |event| match event { diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index 31d30d824e..a51da69903 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -662,7 +662,7 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join( let realtime_server = start_websocket_server_with_headers(vec![WebSocketConnectionConfig { requests: vec![vec![]], response_headers: Vec::new(), - accept_delay: Some(Duration::from_millis(500)), + accept_delay: Some(Duration::from_secs(5)), close_after_requests: false, }]) .await; diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 8687b33ff0..c7e15ba113 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -794,6 +794,8 @@ async fn unified_exec_network_denial_emits_failed_background_end_event() -> Resu let call_id = "uexec-network-denied"; let args = json!({ "cmd": "python3 -c \"import os, socket, time, urllib.parse; time.sleep(0.3); proxy = urllib.parse.urlparse(os.environ['HTTP_PROXY']); sock = socket.create_connection((proxy.hostname, proxy.port), timeout=2); sock.sendall(b'GET http://codex-network-denied.invalid/ HTTP/1.1\\r\\nHost: codex-network-denied.invalid\\r\\n\\r\\n'); sock.recv(1024); time.sleep(5)\"", + "shell": "bash", + "login": false, "yield_time_ms": 50, }); let response_mock = @@ -837,6 +839,8 @@ async fn unified_exec_short_lived_network_denial_emits_failed_end_event() -> Res let call_id = "uexec-short-network-denied"; let args = json!({ "cmd": "python3 -c \"import os, socket, urllib.parse; proxy = urllib.parse.urlparse(os.environ['HTTP_PROXY']); sock = socket.create_connection((proxy.hostname, proxy.port), timeout=2); sock.sendall(b'GET http://codex-short-network-denied.invalid/ HTTP/1.1\\r\\nHost: codex-short-network-denied.invalid\\r\\n\\r\\n'); sock.recv(1024)\"", + "shell": "bash", + "login": false, "yield_time_ms": 1000, }); let response_mock = @@ -912,7 +916,7 @@ allow_local_binding = true )) .expect("set permission profile"); }); - let test = builder.build_with_remote_env(server).await?; + let test = builder.build(server).await?; assert!( test.config.permissions.network.is_some(), "expected managed network proxy config to be present" diff --git a/scripts/test-remote-env.sh b/scripts/test-remote-env.sh index 96743616a2..d7ee5c4d8d 100755 --- a/scripts/test-remote-env.sh +++ b/scripts/test-remote-env.sh @@ -20,12 +20,15 @@ setup_remote_env() { local codex_binary_path local container_ip local remote_codex_path + local remote_codex_linux_sandbox_path local remote_exec_server_pid local remote_exec_server_port local remote_exec_server_stdout_path container_name="${CODEX_TEST_REMOTE_ENV_CONTAINER_NAME:-codex-remote-test-env-local-$(date +%s)-${RANDOM}}" codex_binary_path="${REPO_ROOT}/codex-rs/target/debug/codex" + remote_codex_path="${CODEX_TEST_REMOTE_CODEX_PATH:-/tmp/codex-remote-env/codex}" + remote_codex_linux_sandbox_path="$(dirname "${remote_codex_path}")/codex-linux-sandbox" if ! command -v docker >/dev/null 2>&1; then echo "docker is required (Colima or Docker Desktop)" >&2 @@ -65,12 +68,12 @@ setup_remote_env() { fi if [[ -z "${CODEX_TEST_REMOTE_EXEC_SERVER_URL:-}" ]]; then - remote_codex_path="/tmp/codex-remote-env/codex" remote_exec_server_port="31987" remote_exec_server_stdout_path="/tmp/codex-remote-env/exec-server.stdout" docker exec "${container_name}" sh -lc "mkdir -p /tmp/codex-remote-env" docker cp "${codex_binary_path}" "${container_name}:${remote_codex_path}" - docker exec "${container_name}" chmod +x "${remote_codex_path}" + docker exec "${container_name}" sh -lc \ + "chmod +x ${remote_codex_path} && ln -sf ${remote_codex_path} ${remote_codex_linux_sandbox_path}" remote_exec_server_pid="$( docker exec "${container_name}" sh -lc \ "rm -f ${remote_exec_server_stdout_path}; nohup ${remote_codex_path} exec-server --listen ws://0.0.0.0:${remote_exec_server_port} > ${remote_exec_server_stdout_path} 2>&1 & echo \$!" @@ -89,6 +92,7 @@ setup_remote_env() { fi export CODEX_TEST_REMOTE_ENV="${container_name}" + export CODEX_TEST_REMOTE_CODEX_PATH="${remote_codex_path}" } wait_for_remote_exec_server_port() { @@ -116,6 +120,7 @@ codex_remote_env_cleanup() { fi unset CODEX_TEST_REMOTE_EXEC_SERVER_PID unset CODEX_TEST_REMOTE_EXEC_SERVER_URL + unset CODEX_TEST_REMOTE_CODEX_PATH } if ! is_sourced; then @@ -128,6 +133,7 @@ set -euo pipefail if setup_remote_env; then status=0 echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" + echo "CODEX_TEST_REMOTE_CODEX_PATH=${CODEX_TEST_REMOTE_CODEX_PATH}" echo "CODEX_TEST_REMOTE_EXEC_SERVER_URL=${CODEX_TEST_REMOTE_EXEC_SERVER_URL}" echo "Remote env ready. Run your command, then call: codex_remote_env_cleanup" else