diff --git a/codex-rs/core/tests/remote_env_windows/BUILD.bazel b/codex-rs/core/tests/remote_env_windows/BUILD.bazel index e7dfa36c9f..a97c098660 100644 --- a/codex-rs/core/tests/remote_env_windows/BUILD.bazel +++ b/codex-rs/core/tests/remote_env_windows/BUILD.bazel @@ -2,27 +2,27 @@ load("//bazel/rules/testing:wine.bzl", "wine_rust_test") wine_rust_test( name = "smoke-test", - timeout = "short", - srcs = ["remote_env_windows_test.rs"], + timeout = "moderate", + srcs = [ + "remote_env_windows_test.rs", + "wine_exec_server_harness.rs", + ], crate_name = "remote_env_windows_test", crate_root = "remote_env_windows_test.rs", - # Environment identity validation ends this fixture's host-shell-mismatch premise. - # A hermetic target-shell smoke test replaces it once remote shell routing lands. - target_compatible_with = ["@platforms//:incompatible"], + include_powershell = True, windows_binaries = { "wine-windows-exec-server": "//codex-rs/exec-server/testing:windows-exec-server", }, deps = [ "//bazel/rules/testing/wine:wine_test_support", - "//codex-rs/core/tests/common", "//codex-rs/exec-server", - "//codex-rs/features", "//codex-rs/protocol", "//codex-rs/utils/cargo-bin", "//codex-rs/utils/path-uri", + "//codex-rs/utils/pty", "@crates//:anyhow", "@crates//:pretty_assertions", - "@crates//:serde_json", + "@crates//:tempfile", "@crates//:tokio", ], ) diff --git a/codex-rs/core/tests/remote_env_windows/README.md b/codex-rs/core/tests/remote_env_windows/README.md index 92c9a89739..45c0253e0a 100644 --- a/codex-rs/core/tests/remote_env_windows/README.md +++ b/codex-rs/core/tests/remote_env_windows/README.md @@ -1,8 +1,11 @@ -# Windows remote-environment test +# Windows remote-environment tests -This Bazel-only `test_codex` integration test runs a Windows exec-server fixture -under pinned Wine and exercises the normal model tool-call and remote-execution -path. +This directory cross-builds a small Windows exec-server fixture and runs it +under a pinned Wine runtime from x86-64 Linux Bazel. The Windows fixture links +only `codex-exec-server` because the full Codex Windows graph does not yet +cross-build, and the smaller target is faster to iterate on. The direct smoke +test runs pinned PowerShell and built-in `cmd.exe` through the real exec-server +with Windows cwd URI values. ## Running the test @@ -12,13 +15,18 @@ bazel test \ --test_output=errors ``` -No system Wine is required. Every process gets a fresh `WINEPREFIX` and isolated -wineserver. +No system Wine or apt packages are required. The target is intentionally +limited to x86-64 for simplicity, so Bazel marks it incompatible elsewhere. +Every process gets a fresh `WINEPREFIX` and isolated wineserver. + +The reusable rules and Rust harness live under `//bazel/rules/testing`. Tests +resolve their Windows executable with `codex_utils_cargo_bin::cargo_bin`, then +use `WineTestProcess::scope` for async teardown and panic-safe cleanup. ## Current limitations -- PowerShell and ConPTY/TTY behavior are not yet covered. +- ConPTY/TTY behavior is not yet covered. - Wine loads shared objects and PE DLLs at runtime, so the host must still provide the declared compatible glibc version. -- The target is intentionally limited to x86-64 for simplicity. It can expand - if we find aarch64-specific behavior worth testing. +- ARM64 Linux is intentionally unsupported for now. It is more complicated to + support, and the behavior covered here should not vary by architecture. diff --git a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs index 71f182b401..cc330a66b0 100644 --- a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs +++ b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs @@ -1,206 +1,202 @@ -//! Bazel-only integration coverage for a Windows exec-server running under Wine. +#[cfg(not(target_os = "linux"))] +compile_error!("the Wine exec-server test can only run on Linux"); + +#[path = "wine_exec_server_harness.rs"] +mod wine_exec_server_harness; + +use std::collections::HashMap; +use std::time::Duration; use anyhow::Context; use anyhow::Result; -use codex_exec_server::REMOTE_ENVIRONMENT_ID; -use codex_features::Feature; -use codex_protocol::models::PermissionProfile; -use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::ExecCommandSource; -use codex_protocol::protocol::ExecCommandStatus; -use codex_protocol::protocol::Op; -use codex_protocol::protocol::TurnEnvironmentSelection; -use codex_protocol::protocol::TurnEnvironmentSelections; -use codex_protocol::user_input::UserInput; +use codex_exec_server::ExecEnvPolicy; +use codex_exec_server::ExecParams; +use codex_exec_server::ExecServerClient; +use codex_exec_server::ProcessId; +use codex_exec_server::ReadParams; +use codex_exec_server::RemoteExecServerConnectArgs; +use codex_protocol::config_types::ShellEnvironmentPolicyInherit; +use codex_utils_path_uri::PathConvention; use codex_utils_path_uri::PathUri; -use core_test_support::responses::ev_assistant_message; -use core_test_support::responses::ev_completed; -use core_test_support::responses::ev_function_call; -use core_test_support::responses::ev_response_created; -use core_test_support::responses::mount_sse_sequence; -use core_test_support::responses::sse; -use core_test_support::responses::start_mock_server; -use core_test_support::test_codex::test_codex; -use core_test_support::test_codex::turn_permission_fields; -use core_test_support::wait_for_event; use pretty_assertions::assert_eq; -use serde_json::json; -use tokio::io::AsyncBufReadExt; -use tokio::io::BufReader; -use wine_test_support::WineTestCommand; +use tokio::time::timeout; +use wine_exec_server_harness::POWERSHELL_PATH; +use wine_exec_server_harness::POWERSHELL_VERSION; +use wine_exec_server_harness::WINDOWS_WORKSPACE; +use wine_exec_server_harness::WineExecServer; -const CALL_ID: &str = "wine-cmd-smoke"; -const COMMAND: &str = "echo WINE_BAZEL_OK&&cd"; +const TEST_TIMEOUT: Duration = Duration::from_secs(180); +const POWERSHELL_PREFLIGHT_MARKER: &str = "WINE_PWSH_PREFLIGHT"; +const POWERSHELL_PREFLIGHT_SCRIPT: &str = concat!( + "$ErrorActionPreference = 'Stop'; ", + "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); ", + "$separatorCode = [int]([System.IO.Path]::DirectorySeparatorChar); ", + "Write-Output ('WINE_PWSH_PREFLIGHT|' + ", + "$PSVersionTable.PSVersion.ToString() + '|' + ", + "$PSVersionTable.PSEdition + '|' + ", + "$IsWindows.ToString().ToLowerInvariant() + '|' + ", + "(Get-Location).ProviderPath + '|' + $separatorCode)", +); + +struct CommandOutput { + exit_code: Option, + output: String, +} #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn windows_exec_server_records_host_shell_mismatch() -> Result<()> { - let executable = codex_utils_cargo_bin::cargo_bin("wine-windows-exec-server")?; - let mut exec_server = WineTestCommand::new(executable) - .env("CODEX_HOME", r"C:\codex-home") - .spawn()?; - let stdout = exec_server.take_stdout(); - - exec_server - .scope(async move { - let mut lines = BufReader::new(stdout).lines(); - let exec_server_url = loop { - let line = lines - .next_line() - .await? - .context("Wine exec-server exited before reporting its URL")?; - if line.starts_with("ws://") { - break line; - } - }; - - let server = start_mock_server().await; - let arguments = serde_json::to_string(&json!({ - "cmd": COMMAND, - "login": false, - "yield_time_ms": 5_000, - }))?; - let response_mock = mount_sse_sequence( - &server, - vec![ - sse(vec![ - ev_response_created("resp-1"), - ev_function_call(CALL_ID, "exec_command", &arguments), - ev_completed("resp-1"), - ]), - sse(vec![ - ev_response_created("resp-2"), - ev_assistant_message("msg-1", "done"), - ev_completed("resp-2"), - ]), - ], - ) - .await; - - let mut builder = test_codex() - .with_model("gpt-5.2") - .with_exec_server_url(exec_server_url) - .with_config(|config| { - config.use_experimental_unified_exec_tool = true; - config - .features - .enable(Feature::UnifiedExec) - .expect("test config should allow feature update"); - }); - let test = builder.build(&server).await?; - let (sandbox_policy, permission_profile) = - turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); - let selected_cwd = PathUri::parse("file:///C:/workspace")?; - let environments = TurnEnvironmentSelections::new( - test.config.cwd.clone(), - vec![TurnEnvironmentSelection { - environment_id: REMOTE_ENVIRONMENT_ID.to_string(), - cwd: selected_cwd.clone(), - }], - ); - - test.codex - .submit(Op::UserInput { - items: vec![UserInput::Text { - text: "run the Windows smoke command".to_string(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - responsesapi_client_metadata: None, - additional_context: Default::default(), - thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { - environments: Some(environments), - approval_policy: Some(AskForApproval::Never), - sandbox_policy: Some(sandbox_policy), - permission_profile, - collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { - mode: codex_protocol::config_types::ModeKind::Default, - settings: codex_protocol::config_types::Settings { - model: test.session_configured.model.clone(), - reasoning_effort: None, - developer_instructions: None, - }, - }), - ..Default::default() - }, - }) - .await?; - - let mut begin = None; - let mut end = None; - loop { - match wait_for_event(&test.codex, |_| true).await { - EventMsg::ExecCommandBegin(event) if event.call_id == CALL_ID => { - begin = Some(event) - } - EventMsg::ExecCommandEnd(event) if event.call_id == CALL_ID => { - end = Some(event) - } - EventMsg::TurnComplete(_) => break, - _ => {} - } - } - - let begin = begin.context("exec_command should emit a begin event")?; - let expected_commands = [ - vec![ - "/bin/bash".to_string(), - "-c".to_string(), - COMMAND.to_string(), - ], - vec!["/bin/sh".to_string(), "-c".to_string(), COMMAND.to_string()], - ]; - // This intentionally records the current cross-OS failure mode: the Linux - // orchestrator resolves its own shell before sending the command to the - // Windows exec-server, where that Unix shell cannot start. - assert!( - expected_commands.contains(&begin.command), - "unexpected command: {:?}", - begin.command, - ); - assert_eq!( - (begin.cwd.clone(), begin.source), - (selected_cwd.clone(), ExecCommandSource::UnifiedExecStartup), - ); - - let end = end.context("exec_command should emit an end event")?; - assert_eq!( - ( - end.command, - end.cwd, - end.source, - end.stdout, - end.stderr, - end.aggregated_output, - end.exit_code, - end.status, - ), - ( - begin.command, - selected_cwd, - ExecCommandSource::UnifiedExecStartup, - String::new(), - String::new(), - String::new(), - -1, - ExecCommandStatus::Failed, - ), - ); - - let request = response_mock - .last_request() - .context("model should receive the failed command output")?; - let (output, success) = request - .function_call_output_content_and_success(CALL_ID) - .context("failed command output should be present")?; - let output = output.context("failed command output should contain text")?; - assert!( - output.contains("Process exited with code -1"), - "unexpected command output: {output:?}", - ); - assert_ne!(success, Some(true)); - - Ok(()) - }) - .await +async fn windows_exec_server_runs_powershell_and_cmd_under_wine() -> Result<()> { + timeout(TEST_TIMEOUT, async { + let (server, websocket_url) = WineExecServer::start().await?; + let test_result = exercise_exec_server(websocket_url).await; + let shutdown_result = server.shutdown().await; + test_result?; + shutdown_result + }) + .await + .context("Wine exec-server test timed out")? +} + +async fn exercise_exec_server(websocket_url: String) -> Result<()> { + let client = ExecServerClient::connect_websocket(RemoteExecServerConnectArgs::new( + websocket_url, + "wine-windows-bazel-test".to_string(), + )) + .await?; + + let info = client.environment_info().await?; + assert_eq!(info.path_convention, PathConvention::Windows); + assert_eq!(info.shell.name, "powershell"); + assert!( + info.shell.path.eq_ignore_ascii_case(POWERSHELL_PATH), + "expected pinned PowerShell path, got {info:?}", + ); + + let powershell = run_non_tty_command( + &client, + "wine-pwsh-preflight", + vec![ + info.shell.path, + "-NoLogo".to_string(), + "-NoProfile".to_string(), + "-NonInteractive".to_string(), + "-Command".to_string(), + POWERSHELL_PREFLIGHT_SCRIPT.to_string(), + ], + PathUri::parse("file:///C:/workspace")?, + ) + .await?; + assert_eq!( + powershell.exit_code, + Some(0), + "unexpected PowerShell output: {:?}", + powershell.output + ); + let preflight = powershell + .output + .lines() + .find(|line| line.starts_with(POWERSHELL_PREFLIGHT_MARKER)) + .with_context(|| { + format!( + "PowerShell preflight marker was missing from {:?}", + powershell.output + ) + })?; + assert_eq!( + preflight.split('|').collect::>(), + vec![ + POWERSHELL_PREFLIGHT_MARKER, + POWERSHELL_VERSION, + "Core", + "true", + WINDOWS_WORKSPACE, + "92", + ] + ); + + let cmd = run_non_tty_command( + &client, + "wine-cmd-smoke", + vec![ + r"C:\windows\system32\cmd.exe".to_string(), + "/D".to_string(), + "/C".to_string(), + "echo WINE_BAZEL_OK&&cd".to_string(), + ], + PathUri::parse("file:///C:/workspace")?, + ) + .await?; + assert_eq!(cmd.exit_code, Some(0)); + assert!( + cmd.output.contains("WINE_BAZEL_OK"), + "unexpected output: {:?}", + cmd.output + ); + assert!( + cmd.output.contains(WINDOWS_WORKSPACE), + "unexpected output: {:?}", + cmd.output + ); + + Ok(()) +} + +async fn run_non_tty_command( + client: &ExecServerClient, + process_id: &str, + argv: Vec, + cwd: PathUri, +) -> Result { + let process_id = ProcessId::from(process_id); + let response = client + .exec(ExecParams { + process_id: process_id.clone(), + argv, + cwd, + env_policy: Some(ExecEnvPolicy { + inherit: ShellEnvironmentPolicyInherit::Core, + ignore_default_excludes: true, + exclude: Vec::new(), + r#set: HashMap::new(), + include_only: Vec::new(), + }), + env: HashMap::from([ + ("DOTNET_CLI_TELEMETRY_OPTOUT".to_string(), "1".to_string()), + ("DOTNET_NOLOGO".to_string(), "1".to_string()), + ( + "POWERSHELL_TELEMETRY_OPTOUT".to_string(), + "1".to_string(), + ), + ("POWERSHELL_UPDATECHECK".to_string(), "Off".to_string()), + ]), + tty: false, + pipe_stdin: false, + arg0: None, + }) + .await?; + assert_eq!(response.process_id, process_id); + + let mut after_seq = None; + let mut output = Vec::new(); + let exit_code = loop { + let response = client + .read(ReadParams { + process_id: process_id.clone(), + after_seq, + max_bytes: Some(1024 * 1024), + wait_ms: Some(5_000), + }) + .await?; + for chunk in response.chunks { + output.extend(chunk.chunk.into_inner()); + } + if response.closed { + break response.exit_code; + } + after_seq = response.next_seq.checked_sub(1); + }; + + Ok(CommandOutput { + exit_code, + output: String::from_utf8(output)?, + }) } diff --git a/codex-rs/core/tests/remote_env_windows/wine_exec_server_harness.rs b/codex-rs/core/tests/remote_env_windows/wine_exec_server_harness.rs new file mode 100644 index 0000000000..3b3962cf22 --- /dev/null +++ b/codex-rs/core/tests/remote_env_windows/wine_exec_server_harness.rs @@ -0,0 +1,266 @@ +use std::collections::HashMap; +use std::io::Write; +use std::path::PathBuf; +use std::process::Stdio; +use std::time::Duration; +use std::time::Instant; + +use anyhow::Context; +use anyhow::Result; +use codex_utils_pty::ProcessHandle; +use codex_utils_pty::SpawnedProcess; +use codex_utils_pty::TerminalSize; +use tempfile::TempDir; +use tokio::process::Command; +use tokio::sync::oneshot; +use tokio::task::JoinHandle; +use tokio::time::timeout; + +const START_TIMEOUT: Duration = Duration::from_secs(30); +const BLOCKING_CLEANUP_TIMEOUT: Duration = Duration::from_secs(5); + +pub(crate) const POWERSHELL_PATH: &str = r"C:\Program Files\PowerShell\7\pwsh.exe"; +pub(crate) const POWERSHELL_VERSION: &str = "7.2.24"; +pub(crate) const WINDOWS_WORKSPACE: &str = r"C:\workspace"; + +pub(crate) struct WineExecServer { + processes: Option, +} + +struct WineProcesses { + cleanup_complete: bool, + exit_rx: Option>, + prefix: TempDir, + process: ProcessHandle, + stderr_task: JoinHandle<()>, + stdout_task: JoinHandle<()>, + wineserver: PathBuf, +} + +impl WineExecServer { + pub(crate) async fn start() -> Result<(Self, String)> { + let prefix = TempDir::new().context("create Wine prefix")?; + wine_test_support::install_pinned_powershell_runtime(prefix.path())?; + + let executable = codex_utils_cargo_bin::cargo_bin("wine-windows-exec-server")?; + let wine = codex_utils_cargo_bin::cargo_bin("wine")?; + let wine_runtime_marker = codex_utils_cargo_bin::cargo_bin("wine-runtime-marker")?; + let wine_dll_path = wine_runtime_marker + .parent() + .context("locate Wine runtime directory")?; + let wineserver = codex_utils_cargo_bin::cargo_bin("wineserver")?; + let mut env = std::env::vars().collect::>(); + env.remove("DISPLAY"); + env.extend([ + ("HOME".to_string(), prefix.path().to_string_lossy().into_owned()), + ( + "XDG_RUNTIME_DIR".to_string(), + prefix.path().to_string_lossy().into_owned(), + ), + ("WINEARCH".to_string(), "win64".to_string()), + ( + "WINEPREFIX".to_string(), + prefix.path().to_string_lossy().into_owned(), + ), + ( + "WINEDLLPATH".to_string(), + wine_dll_path.to_string_lossy().into_owned(), + ), + ( + "WINESERVER".to_string(), + wineserver.to_string_lossy().into_owned(), + ), + ("WINEDEBUG".to_string(), "-all".to_string()), + ( + "WINEDLLOVERRIDES".to_string(), + "mscoree,mshtml,winegstreamer=".to_string(), + ), + ("LANG".to_string(), "C.UTF-8".to_string()), + ("LC_ALL".to_string(), "C.UTF-8".to_string()), + ("LC_CTYPE".to_string(), "C.UTF-8".to_string()), + ("TEMP".to_string(), r"C:\windows\temp".to_string()), + ("TMP".to_string(), r"C:\windows\temp".to_string()), + ("CODEX_HOME".to_string(), r"C:\codex-home".to_string()), + ("DOTNET_CLI_TELEMETRY_OPTOUT".to_string(), "1".to_string()), + ("DOTNET_NOLOGO".to_string(), "1".to_string()), + ( + "POWERSHELL_TELEMETRY_OPTOUT".to_string(), + "1".to_string(), + ), + ("POWERSHELL_UPDATECHECK".to_string(), "Off".to_string()), + ]); + let wine = wine.to_string_lossy().into_owned(); + let executable = executable.to_string_lossy().into_owned(); + let SpawnedProcess { + session: process, + mut stdout_rx, + mut stderr_rx, + exit_rx, + } = codex_utils_pty::spawn_pty_process( + &wine, + &[executable], + prefix.path(), + &env, + /*arg0*/ &None, + TerminalSize::default(), + ) + .await + .context("start Windows exec-server under Wine")?; + let stderr_task = tokio::spawn(async move { + while let Some(chunk) = stderr_rx.recv().await { + let _ = std::io::stderr().lock().write_all(&chunk); + } + }); + let websocket_url_result = timeout(START_TIMEOUT, async { + let mut output = Vec::new(); + loop { + let chunk = stdout_rx + .recv() + .await + .context("Wine exec-server exited before reporting its URL")?; + output.extend_from_slice(&chunk); + if output.len() > 64 * 1024 { + output.drain(..output.len() - 64 * 1024); + } + let rendered = String::from_utf8_lossy(&output); + if let Some(start) = rendered.find("ws://") { + let url = &rendered[start..]; + let end = url.find(char::is_whitespace).unwrap_or(url.len()); + return Ok::<_, anyhow::Error>(url[..end].to_string()); + } + } + }) + .await + .context("Wine exec-server startup timed out") + .and_then(std::convert::identity); + let stdout_task = tokio::spawn(async move { while stdout_rx.recv().await.is_some() {} }); + let server = Self { + processes: Some(WineProcesses { + cleanup_complete: false, + exit_rx: Some(exit_rx), + prefix, + process, + stderr_task, + stdout_task, + wineserver, + }), + }; + + match websocket_url_result { + Ok(websocket_url) => Ok((server, websocket_url)), + Err(start_error) => { + if let Err(shutdown_error) = server.shutdown().await { + return Err(start_error.context(format!( + "Wine cleanup after startup failure also failed: {shutdown_error:#}" + ))); + } + Err(start_error) + } + } + } + + pub(crate) async fn shutdown(mut self) -> Result<()> { + let result = self + .processes + .as_mut() + .context("Wine process guard is missing")? + .shutdown() + .await; + self.processes.take(); + result + } +} + +impl Drop for WineExecServer { + fn drop(&mut self) { + if self.processes.is_some() && !std::thread::panicking() { + panic!("WineExecServer dropped without calling async shutdown"); + } + } +} + +impl WineProcesses { + async fn shutdown(&mut self) -> Result<()> { + self.process.request_terminate(); + let wait_result = async { + let exit_rx = self + .exit_rx + .take() + .context("Wine process exit receiver is missing")?; + timeout(START_TIMEOUT, exit_rx) + .await + .context("wait for Windows exec-server process timed out")? + .context("wait for Windows exec-server process")?; + Ok::<_, anyhow::Error>(()) + } + .await; + self.stderr_task.abort(); + self.stdout_task.abort(); + let wineserver_result = timeout(START_TIMEOUT, async { + let status = Command::new(&self.wineserver) + .args(["-k", "-w"]) + .env("HOME", self.prefix.path()) + .env("WINEPREFIX", self.prefix.path()) + .env("XDG_RUNTIME_DIR", self.prefix.path()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .await + .context("stop isolated wineserver")?; + anyhow::ensure!(status.success(), "wineserver exited with {status}"); + Ok::<_, anyhow::Error>(()) + }) + .await + .context("stop isolated wineserver timed out") + .and_then(std::convert::identity); + + let result = wait_result.and(wineserver_result); + if result.is_ok() { + self.cleanup_complete = true; + } else { + self.shutdown_blocking(); + } + result + } + + fn shutdown_blocking(&mut self) { + self.stderr_task.abort(); + self.stdout_task.abort(); + self.process.terminate(); + let Ok(mut child) = std::process::Command::new(&self.wineserver) + .arg("-k") + .env("HOME", self.prefix.path()) + .env("WINEPREFIX", self.prefix.path()) + .env("XDG_RUNTIME_DIR", self.prefix.path()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + else { + self.cleanup_complete = true; + return; + }; + let deadline = Instant::now() + BLOCKING_CLEANUP_TIMEOUT; + loop { + match child.try_wait() { + Ok(Some(_)) => break, + Ok(None) if Instant::now() < deadline => { + std::thread::sleep(Duration::from_millis(50)); + } + Ok(None) | Err(_) => { + let _ = child.kill(); + let _ = child.wait(); + break; + } + } + } + self.cleanup_complete = true; + } +} + +impl Drop for WineProcesses { + fn drop(&mut self) { + if !self.cleanup_complete && std::thread::panicking() { + self.shutdown_blocking(); + } + } +} diff --git a/codex-rs/exec-server/testing/README.md b/codex-rs/exec-server/testing/README.md index 77f8e990d4..f6d6486300 100644 --- a/codex-rs/exec-server/testing/README.md +++ b/codex-rs/exec-server/testing/README.md @@ -1,5 +1,4 @@ -# Windows exec-server fixture +# Exec-server test fixtures -This directory contains the small Windows exec-server binary used by -foreign-OS tests. It links only `codex-exec-server` because the full Codex -Windows graph does not yet cross-build with Bazel. +This package contains the minimal Windows exec-server binary used by the +cross-platform tests in `//codex-rs/core/tests/remote_env_windows`. diff --git a/codex-rs/exec-server/testing/windows_exec_server.rs b/codex-rs/exec-server/testing/windows_exec_server.rs index 9efc5286fc..c9ad9c05c9 100644 --- a/codex-rs/exec-server/testing/windows_exec_server.rs +++ b/codex-rs/exec-server/testing/windows_exec_server.rs @@ -9,6 +9,7 @@ use codex_exec_server::ExecServerRuntimePaths; #[tokio::main] async fn main() -> Result<(), Box> { + std::fs::create_dir_all(r"C:\workspace")?; let current_exe = std::env::current_exe()?; // This fixture is always a Windows executable, so it neither invokes nor // needs the separate Linux sandbox binary.