diff --git a/.codex/skills/remote-tests/SKILL.md b/.codex/skills/remote-tests/SKILL.md index ee35fc2b2..f0253370d 100644 --- a/.codex/skills/remote-tests/SKILL.md +++ b/.codex/skills/remote-tests/SKILL.md @@ -3,12 +3,15 @@ name: remote-tests description: How to run tests using remote executor. --- -Some codex integration tests support a running against a remote executor. -This means that when CODEX_TEST_REMOTE_ENV environment variable is set they will attempt to start an executor process in a docker container CODEX_TEST_REMOTE_ENV points to and use it in tests. +Some Codex integration tests select `local`, `docker`, or `wine-exec` through +`CODEX_TEST_ENVIRONMENT`. The legacy `CODEX_TEST_REMOTE_ENV=` still +selects Docker; otherwise execution is local. Docker container is built and initialized via ./scripts/test-remote-env.sh -Currently running remote tests is only supported on Linux, so you need to use a devbox to run them +On x86-64 Linux, run Wine exec with +`bazel test //codex-rs/core:core-all-wine-exec-test --test_output=errors`. +Temporary blockers belong beside the test in `skip_if_wine_exec!` calls. You can list devboxes via `applied_devbox ls`, pick the one with `codex` in the name. Connect to devbox via `ssh `. diff --git a/.github/workflows/rust-ci-full-nextest-platform.yml b/.github/workflows/rust-ci-full-nextest-platform.yml index 3fdf7b51e..65d1fb2be 100644 --- a/.github/workflows/rust-ci-full-nextest-platform.yml +++ b/.github/workflows/rust-ci-full-nextest-platform.yml @@ -343,7 +343,9 @@ jobs: set -euo pipefail export CODEX_TEST_REMOTE_ENV_CONTAINER_NAME="codex-remote-test-env-${{ github.run_id }}-${{ matrix.shard }}" source "${GITHUB_WORKSPACE}/scripts/test-remote-env.sh" + echo "CODEX_TEST_ENVIRONMENT=${CODEX_TEST_ENVIRONMENT}" >> "$GITHUB_ENV" echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" >> "$GITHUB_ENV" + echo "CODEX_TEST_REMOTE_ENV_CONTAINER_NAME=${CODEX_TEST_REMOTE_ENV_CONTAINER_NAME}" >> "$GITHUB_ENV" echo "CODEX_TEST_REMOTE_EXEC_SERVER_URL=${CODEX_TEST_REMOTE_EXEC_SERVER_URL}" >> "$GITHUB_ENV" - name: Download nextest archive @@ -437,9 +439,9 @@ jobs: run: | set +e if [[ "${STEPS_TEST_OUTCOME}" != "success" ]]; then - docker logs "${CODEX_TEST_REMOTE_ENV}" || true + docker logs "${CODEX_TEST_REMOTE_ENV_CONTAINER_NAME}" || true fi - docker rm -f "${CODEX_TEST_REMOTE_ENV}" >/dev/null 2>&1 || true + docker rm -f "${CODEX_TEST_REMOTE_ENV_CONTAINER_NAME}" >/dev/null 2>&1 || true env: STEPS_TEST_OUTCOME: ${{ steps.test.outcome }} diff --git a/bazel/rules/testing/BUILD.bazel b/bazel/rules/testing/BUILD.bazel index 821c3a7aa..921cfacc8 100644 --- a/bazel/rules/testing/BUILD.bazel +++ b/bazel/rules/testing/BUILD.bazel @@ -2,5 +2,4 @@ package(default_visibility = ["//visibility:public"]) exports_files([ "foreign_platform_binary.bzl", - "wine.bzl", ]) diff --git a/bazel/rules/testing/wine/BUILD.bazel b/bazel/rules/testing/wine/BUILD.bazel index a19b29fd1..4bd9acf6d 100644 --- a/bazel/rules/testing/wine/BUILD.bazel +++ b/bazel/rules/testing/wine/BUILD.bazel @@ -1,8 +1,13 @@ load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") -load("//bazel/rules/testing:wine.bzl", "wine_rust_test") +load(":wine.bzl", "wine_rust_test") package(default_visibility = ["//visibility:public"]) +exports_files([ + "wine.bzl", + "wine_runtime.bzl", +]) + rust_library( name = "wine_test_support", testonly = True, diff --git a/bazel/rules/testing/wine.bzl b/bazel/rules/testing/wine/wine.bzl similarity index 66% rename from bazel/rules/testing/wine.bzl rename to bazel/rules/testing/wine/wine.bzl index 9f0fbade1..94b0d7a96 100644 --- a/bazel/rules/testing/wine.bzl +++ b/bazel/rules/testing/wine/wine.bzl @@ -2,15 +2,8 @@ load("@rules_rust//rust:defs.bzl", "rust_test") load("//:defs.bzl", "WINDOWS_GNULLVM_RUSTC_LINK_FLAGS") -load(":foreign_platform_binary.bzl", "foreign_platform_binary") - -_WINE_RUNTIME_BINARIES = { - "pwsh": "@powershell_windows_x86_64//:pwsh", - "pwsh-runtime-marker": "@powershell_windows_x86_64//:runtime_marker", - "wine": "@wine_linux_x86_64//:wine", - "wine-runtime-marker": "@wine_linux_x86_64//:runtime_marker", - "wineserver": "@wine_linux_x86_64//:wineserver", -} +load("//bazel/rules/testing:foreign_platform_binary.bzl", "foreign_platform_binary") +load(":wine_runtime.bzl", "WINE_TEST_TARGET_COMPATIBLE_WITH", "wine_test_runtime") def wine_rust_test( name, @@ -46,20 +39,10 @@ def wine_rust_test( target_compatible_with: Additional compatibility constraints. **kwargs: Remaining attributes forwarded to `rust_test`. """ - binaries = dict(_WINE_RUNTIME_BINARIES) - runtime_data = [ - "@powershell_windows_x86_64//:runtime", - "@wine_linux_x86_64//:runtime", - ] - - for binary_name in sorted(host_binaries.keys()): - if binary_name in binaries: - fail("host test binary name collides with test runtime: {}".format(binary_name)) - binaries[binary_name] = host_binaries[binary_name] - + binaries = dict(host_binaries) for index, binary_name in enumerate(sorted(windows_binaries.keys())): if binary_name in binaries: - fail("Windows test binary name collides with existing binary: {}".format(binary_name)) + fail("Windows test binary name collides with host binary: {}".format(binary_name)) transitioned_binary = name + "-windows-binary-" + str(index) foreign_platform_binary( name = transitioned_binary, @@ -76,17 +59,11 @@ def wine_rust_test( ) binaries[binary_name] = ":" + transitioned_binary + runtime = wine_test_runtime(binaries) rust_test( name = name, - data = data + runtime_data + [binary for binary in binaries.values()], - env = { - "CARGO_BIN_EXE_{}".format(binary_name): "$(rlocationpath {})".format(binary) - for binary_name, binary in binaries.items() - }, - target_compatible_with = target_compatible_with + [ - "@llvm//constraints/libc:gnu.2.28", - "@platforms//cpu:x86_64", - "@platforms//os:linux", - ], + data = data + runtime.data, + env = runtime.env, + target_compatible_with = target_compatible_with + WINE_TEST_TARGET_COMPATIBLE_WITH, **kwargs ) diff --git a/bazel/rules/testing/wine/wine_runtime.bzl b/bazel/rules/testing/wine/wine_runtime.bzl new file mode 100644 index 000000000..9f47bdcff --- /dev/null +++ b/bazel/rules/testing/wine/wine_runtime.bzl @@ -0,0 +1,40 @@ +"""Runfiles shared by tests that execute Windows binaries through Wine.""" + +_WINE_RUNTIME_BINARIES = { + "pwsh": "@powershell_windows_x86_64//:pwsh", + "pwsh-runtime-marker": "@powershell_windows_x86_64//:runtime_marker", + "wine": "@wine_linux_x86_64//:wine", + "wine-runtime-marker": "@wine_linux_x86_64//:runtime_marker", + "wineserver": "@wine_linux_x86_64//:wineserver", +} + +_WINE_RUNTIME_DATA = [ + "@powershell_windows_x86_64//:runtime", + "@wine_linux_x86_64//:runtime", +] + +WINE_TEST_TARGET_COMPATIBLE_WITH = [ + "@llvm//constraints/libc:gnu.2.28", + "@platforms//cpu:x86_64", + "@platforms//os:linux", +] + +def wine_test_runtime(test_binaries = {}): + """Returns data and environment mappings for a Wine-backed test.""" + binaries = dict(_WINE_RUNTIME_BINARIES) + for binary_name in sorted(test_binaries.keys()): + if binary_name in binaries: + fail("test binary name collides with Wine runtime: {}".format(binary_name)) + binaries[binary_name] = test_binaries[binary_name] + + return struct( + data = _WINE_RUNTIME_DATA + [binary for binary in binaries.values()], + env = { + "CARGO_BIN_EXE_{}".format(binary_name): "$(rlocationpath {})".format(binary) + for binary_name, binary in binaries.items() + }, + runfile_env = { + binary_label: "CARGO_BIN_EXE_" + binary_name + for binary_name, binary_label in binaries.items() + }, + ) diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index 34c2396da..a55a98e18 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -22,6 +22,7 @@ codex_rust_crate( "//codex-rs/windows-sandbox-rs:codex-windows-sandbox-setup", ], integration_test_timeout = "long", + run_tests_with_wine_exec = True, rustc_env = { # Keep manifest-root path lookups inside the Bazel execroot for code # that relies on env!("CARGO_MANIFEST_DIR"). diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 57b9e53f6..e992059b5 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -2,6 +2,12 @@ This crate implements the business logic for Codex. It is designed to be used by the various Codex UIs written in Rust. +## Wine-exec integration tests + +On x86-64 Linux, run the shared suite against the Windows exec server with +`bazel test //codex-rs/core:core-all-wine-exec-test`. Temporary blockers use a +source-local `skip_if_wine_exec!` call and reason. + ## Dependencies Note that `codex-core` makes some assumptions about certain helper utilities being available in the environment. Currently, this support matrix is: diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index f9732b1b5..f23522777 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -32,9 +32,14 @@ pub mod responses; pub mod streaming_sse; pub mod test_codex; pub mod test_codex_exec; +mod test_environment; pub mod tracing; pub mod zsh_fork; +pub use test_environment::TestEnvironment; +pub use test_environment::get_remote_test_env; +pub use test_environment::test_environment; + static TEST_ARG0_PATH_ENTRY: OnceLock> = OnceLock::new(); #[ctor] @@ -357,33 +362,6 @@ pub fn sandbox_network_env_var() -> &'static str { codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR } -const REMOTE_ENV_ENV_VAR: &str = "CODEX_TEST_REMOTE_ENV"; - -pub fn remote_env_env_var() -> &'static str { - REMOTE_ENV_ENV_VAR -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct RemoteEnvConfig { - pub container_name: String, -} - -pub fn get_remote_test_env() -> Option { - if std::env::var_os(REMOTE_ENV_ENV_VAR).is_none() { - eprintln!("Skipping test because {REMOTE_ENV_ENV_VAR} is not set."); - return None; - } - - let container_name = std::env::var(REMOTE_ENV_ENV_VAR) - .unwrap_or_else(|_| panic!("{REMOTE_ENV_ENV_VAR} must be set")); - assert!( - !container_name.trim().is_empty(), - "{REMOTE_ENV_ENV_VAR} must not be empty" - ); - - Some(RemoteEnvConfig { container_name }) -} - pub fn format_with_current_shell(command: &str) -> Vec { codex_core::shell::default_user_shell().derive_exec_args(command, /*use_login_shell*/ true) } @@ -597,30 +575,58 @@ macro_rules! skip_if_no_network { }}; } +// Exported so the public skip macros can expand in downstream test crates. +// Call `skip_if_remote!` or `skip_if_wine_exec!` instead. #[macro_export] -macro_rules! skip_if_remote { - ($reason:expr $(,)?) => {{ - if ::std::env::var_os($crate::remote_env_env_var()).is_some() { - eprintln!( - "Skipping test under {}: {}", - $crate::remote_env_env_var(), - $reason - ); +#[doc(hidden)] +macro_rules! skip_if_test_environment { + ($pattern:pat, $reason:expr $(,)?) => {{ + let environment = $crate::test_environment(); + if ::std::matches!(&environment, $pattern) { + eprintln!("Skipping test in {environment:?}: {}", $reason); return; } }}; - ($return_value:expr, $reason:expr $(,)?) => {{ - if ::std::env::var_os($crate::remote_env_env_var()).is_some() { - eprintln!( - "Skipping test under {}: {}", - $crate::remote_env_env_var(), - $reason - ); + ($return_value:expr, $pattern:pat, $reason:expr $(,)?) => {{ + let environment = $crate::test_environment(); + if ::std::matches!(&environment, $pattern) { + eprintln!("Skipping test in {environment:?}: {}", $reason); return $return_value; } }}; } +#[macro_export] +macro_rules! skip_if_remote { + ($reason:expr $(,)?) => {{ + $crate::skip_if_test_environment!( + $crate::TestEnvironment::Docker { .. } | $crate::TestEnvironment::WineExec, + $reason, + ); + }}; + ($return_value:expr, $reason:expr $(,)?) => {{ + $crate::skip_if_test_environment!( + $return_value, + $crate::TestEnvironment::Docker { .. } | $crate::TestEnvironment::WineExec, + $reason, + ); + }}; +} + +#[macro_export] +macro_rules! skip_if_wine_exec { + ($reason:expr $(,)?) => {{ + $crate::skip_if_test_environment!($crate::TestEnvironment::WineExec, $reason); + }}; + ($return_value:expr, $reason:expr $(,)?) => {{ + $crate::skip_if_test_environment!( + $return_value, + $crate::TestEnvironment::WineExec, + $reason, + ); + }}; +} + #[macro_export] macro_rules! codex_linux_sandbox_exe_or_skip { () => {{ diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 477e325f5..1d9585652 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -53,7 +53,6 @@ use serde_json::Value; use tempfile::TempDir; use wiremock::MockServer; -use crate::PathBufExt; use crate::TempDirExt; use crate::get_remote_test_env; use crate::load_default_config_for_test; @@ -164,8 +163,10 @@ pub async fn test_env() -> Result { let websocket_url = remote_exec_server_url()?; let environment = codex_exec_server::Environment::create_for_tests(Some(websocket_url.clone()))?; - let cwd = remote_aware_cwd_path(); - let cwd_uri = PathUri::from_path(&cwd)?; + let cwd = remote_env + .remote_cwd(&remote_test_instance_id())? + .context("remote test environment should define a cwd")?; + let cwd_uri = cwd.to_path_uri(remote_env.path_convention())?; environment .get_filesystem() .create_directory( @@ -174,26 +175,19 @@ pub async fn test_env() -> Result { /*sandbox*/ None, ) .await?; + let cwd = cwd_uri.to_abs_path()?; Ok(TestEnv { environment, exec_server_url: Some(websocket_url), cwd, local_cwd_temp_dir: None, - remote_container_name: Some(remote_env.container_name), + remote_container_name: remote_env.docker_container_name().map(str::to_owned), }) } None => TestEnv::local().await, } } -fn remote_aware_cwd_path() -> AbsolutePathBuf { - PathBuf::from(format!( - "/tmp/codex-core-test-cwd-{}", - remote_test_instance_id() - )) - .abs() -} - fn remote_exec_server_url() -> Result { let listen_url = std::env::var(REMOTE_EXEC_SERVER_URL_ENV_VAR).with_context(|| { format!("{REMOTE_EXEC_SERVER_URL_ENV_VAR} must be set for remote tests") diff --git a/codex-rs/core/tests/common/test_environment.rs b/codex-rs/core/tests/common/test_environment.rs new file mode 100644 index 000000000..addc3b0b7 --- /dev/null +++ b/codex-rs/core/tests/common/test_environment.rs @@ -0,0 +1,134 @@ +use std::ffi::OsStr; + +use anyhow::Result; +use codex_utils_path_uri::ApiPathString; +use codex_utils_path_uri::PathConvention; +use codex_utils_path_uri::PathUri; + +pub const TEST_ENVIRONMENT_ENV_VAR: &str = "CODEX_TEST_ENVIRONMENT"; +pub const LEGACY_REMOTE_ENV_ENV_VAR: &str = "CODEX_TEST_REMOTE_ENV"; +pub const DOCKER_CONTAINER_ENV_VAR: &str = "CODEX_TEST_REMOTE_ENV_CONTAINER_NAME"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TestEnvironment { + Local, + Docker { container_name: String }, + WineExec, +} + +impl TestEnvironment { + pub fn is_remote(&self) -> bool { + !matches!(self, Self::Local) + } + + pub fn docker_container_name(&self) -> Option<&str> { + match self { + Self::Docker { container_name } => Some(container_name), + Self::Local | Self::WineExec => None, + } + } + + pub(crate) fn remote_cwd(&self, instance_id: &str) -> Result> { + let path_uri = match self { + Self::Local => return Ok(None), + Self::Docker { .. } => { + PathUri::parse(&format!("file:///tmp/codex-core-test-cwd-{instance_id}"))? + } + Self::WineExec => { + // Each Wine-exec test process has an isolated filesystem root, so this drive-root + // path cannot collide with a different Bazel shard. + PathUri::parse(&format!("file:///C:/codex-core-test-cwd-{instance_id}"))? + } + }; + Ok(Some(ApiPathString::from_path_uri( + &path_uri, + self.path_convention(), + )?)) + } + + pub(crate) fn path_convention(&self) -> PathConvention { + match self { + Self::Local => PathConvention::native(), + Self::Docker { .. } => PathConvention::Posix, + Self::WineExec => PathConvention::Windows, + } + } +} + +pub fn test_environment() -> TestEnvironment { + let environment = parse_test_environment( + std::env::var_os(TEST_ENVIRONMENT_ENV_VAR).as_deref(), + std::env::var_os(LEGACY_REMOTE_ENV_ENV_VAR).as_deref(), + std::env::var_os(DOCKER_CONTAINER_ENV_VAR).as_deref(), + ) + .unwrap_or_else(|error| panic!("invalid test environment configuration: {error}")); + + if matches!(environment, TestEnvironment::WineExec) && !cfg!(target_os = "linux") { + panic!("{TEST_ENVIRONMENT_ENV_VAR}=wine-exec is only supported on Linux"); + } + + environment +} + +pub fn get_remote_test_env() -> Option { + let environment = test_environment(); + environment.is_remote().then_some(environment) +} + +fn parse_test_environment( + configured_environment: Option<&OsStr>, + legacy_remote_environment: Option<&OsStr>, + docker_container: Option<&OsStr>, +) -> Result { + let configured_environment = configured_environment + .map(|value| { + value + .to_str() + .ok_or_else(|| format!("{TEST_ENVIRONMENT_ENV_VAR} must contain valid UTF-8")) + }) + .transpose()?; + + match configured_environment { + None => match legacy_remote_environment { + Some(container_name) => Ok(TestEnvironment::Docker { + container_name: non_empty_utf8(LEGACY_REMOTE_ENV_ENV_VAR, container_name)?, + }), + None => Ok(TestEnvironment::Local), + }, + Some("local") => Ok(TestEnvironment::Local), + Some("docker") => { + let (container_name_env_var, container_name) = match docker_container { + Some(container_name) => (DOCKER_CONTAINER_ENV_VAR, container_name), + None => ( + LEGACY_REMOTE_ENV_ENV_VAR, + legacy_remote_environment.ok_or_else(|| { + format!( + "{DOCKER_CONTAINER_ENV_VAR} must be set when {TEST_ENVIRONMENT_ENV_VAR}=docker" + ) + })?, + ), + }; + Ok(TestEnvironment::Docker { + container_name: non_empty_utf8(container_name_env_var, container_name)?, + }) + } + Some("wine-exec") => Ok(TestEnvironment::WineExec), + Some(value) => Err(format!( + "{TEST_ENVIRONMENT_ENV_VAR} must be one of local, docker, or wine-exec; got {value:?}" + )), + } +} + +fn non_empty_utf8(name: &str, value: &OsStr) -> Result { + let value = value + .to_str() + .ok_or_else(|| format!("{name} must contain valid UTF-8"))?; + if value.trim().is_empty() { + return Err(format!("{name} must not be empty")); + } + Ok(value.to_string()) +} + +#[cfg(test)] +#[path = "test_environment_tests.rs"] +mod tests; diff --git a/codex-rs/core/tests/common/test_environment_tests.rs b/codex-rs/core/tests/common/test_environment_tests.rs new file mode 100644 index 000000000..9c16ff85b --- /dev/null +++ b/codex-rs/core/tests/common/test_environment_tests.rs @@ -0,0 +1,118 @@ +use std::ffi::OsStr; + +use pretty_assertions::assert_eq; + +use super::*; + +#[test] +fn defaults_to_local() { + assert_eq!( + parse_test_environment( + /*configured_environment*/ None, /*legacy_remote_environment*/ None, + /*docker_container*/ None, + ), + Ok(TestEnvironment::Local) + ); +} + +#[test] +fn parses_each_explicit_environment() { + assert_eq!( + parse_test_environment( + Some(OsStr::new("local")), + /*legacy_remote_environment*/ None, + /*docker_container*/ None, + ), + Ok(TestEnvironment::Local) + ); + assert_eq!( + parse_test_environment( + Some(OsStr::new("docker")), + /*legacy_remote_environment*/ None, + Some(OsStr::new("container-1")), + ), + Ok(TestEnvironment::Docker { + container_name: "container-1".to_string(), + }) + ); + assert_eq!( + parse_test_environment( + Some(OsStr::new("wine-exec")), + /*legacy_remote_environment*/ None, + /*docker_container*/ None, + ), + Ok(TestEnvironment::WineExec) + ); +} + +#[test] +fn treats_the_legacy_remote_value_as_a_docker_container() { + assert_eq!( + parse_test_environment( + /*configured_environment*/ None, + Some(OsStr::new("legacy-container")), + /*docker_container*/ None, + ), + Ok(TestEnvironment::Docker { + container_name: "legacy-container".to_string(), + }) + ); +} + +#[test] +fn explicit_docker_accepts_the_legacy_container_value() { + assert_eq!( + parse_test_environment( + Some(OsStr::new("docker")), + Some(OsStr::new("legacy-container")), + /*docker_container*/ None, + ), + Ok(TestEnvironment::Docker { + container_name: "legacy-container".to_string(), + }) + ); + assert_eq!( + parse_test_environment( + Some(OsStr::new("docker")), + Some(OsStr::new("")), + /*docker_container*/ None, + ), + Err(format!("{LEGACY_REMOTE_ENV_ENV_VAR} must not be empty")) + ); +} + +#[test] +fn explicit_local_ignores_stale_remote_metadata() { + assert_eq!( + parse_test_environment( + Some(OsStr::new("local")), + Some(OsStr::new("legacy-container")), + Some(OsStr::new("container-1")), + ), + Ok(TestEnvironment::Local) + ); +} + +#[test] +fn rejects_invalid_or_incomplete_configuration() { + assert_eq!( + parse_test_environment( + Some(OsStr::new("docker")), + /*legacy_remote_environment*/ None, + /*docker_container*/ None, + ), + Err(format!( + "{DOCKER_CONTAINER_ENV_VAR} must be set when {TEST_ENVIRONMENT_ENV_VAR}=docker" + )) + ); + assert_eq!( + parse_test_environment( + Some(OsStr::new("other")), + /*legacy_remote_environment*/ None, + /*docker_container*/ None, + ), + Err(format!( + "{TEST_ENVIRONMENT_ENV_VAR} must be one of local, docker, or wine-exec; got \"other\"" + )) + ); +} diff --git a/codex-rs/core/tests/remote_env_windows/BUILD.bazel b/codex-rs/core/tests/remote_env_windows/BUILD.bazel index de7dcfaa7..9b5ce52bd 100644 --- a/codex-rs/core/tests/remote_env_windows/BUILD.bazel +++ b/codex-rs/core/tests/remote_env_windows/BUILD.bazel @@ -1,4 +1,4 @@ -load("//bazel/rules/testing:wine.bzl", "wine_rust_test") +load("//bazel/rules/testing/wine:wine.bzl", "wine_rust_test") wine_rust_test( name = "smoke-test", diff --git a/codex-rs/core/tests/remote_env_windows/README.md b/codex-rs/core/tests/remote_env_windows/README.md index 92c9a8973..f2bc1e380 100644 --- a/codex-rs/core/tests/remote_env_windows/README.md +++ b/codex-rs/core/tests/remote_env_windows/README.md @@ -17,7 +17,7 @@ wineserver. ## 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 diff --git a/codex-rs/core/tests/suite/agents_md.rs b/codex-rs/core/tests/suite/agents_md.rs index 05fea8415..4950ee638 100644 --- a/codex-rs/core/tests/suite/agents_md.rs +++ b/codex-rs/core/tests/suite/agents_md.rs @@ -25,6 +25,7 @@ use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; +use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::RecordingUserInstructionsProvider; use core_test_support::test_codex::TestCodexBuilder; use core_test_support::test_codex::test_codex; @@ -138,6 +139,8 @@ fn request_body_contains(request: &wiremock::Request, text: &str) -> bool { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn agents_override_is_preferred_over_agents_md() -> Result<()> { + // TODO(anp): Remove after instruction-source helpers use target-native paths. + skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths"); let instructions = agents_instructions(test_codex().with_workspace_setup(|cwd, fs| async move { let agents_md = cwd.join("AGENTS.md"); @@ -170,6 +173,8 @@ async fn agents_override_is_preferred_over_agents_md() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn configured_fallback_is_used_when_agents_candidate_is_directory() -> Result<()> { + // TODO(anp): Remove after instruction-source helpers use target-native paths. + skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths"); let instructions = agents_instructions( test_codex() .with_config(|config| { @@ -351,6 +356,8 @@ async fn symlinked_cwd_uses_logical_parent_for_agents_discovery() -> Result<()> #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn selected_environment_sources_match_model_visible_instructions() -> Result<()> { + // TODO(anp): Remove after instruction-source helpers use target-native paths. + skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths"); let server = start_mock_server().await; let resp_mock = mount_sse_once( &server, @@ -478,6 +485,8 @@ async fn loads_user_instructions_without_a_primary_environment() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn fresh_thread_composes_global_before_project_and_reports_sources() -> Result<()> { + // TODO(anp): Remove after instruction-source helpers use target-native paths. + skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths"); // Set up one global source, one project source, and two ordinary model turns. let server = responses::start_mock_server().await; let response_mock = responses::mount_sse_sequence( @@ -588,6 +597,8 @@ async fn fresh_thread_composes_global_before_project_and_reports_sources() -> Re #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapshot() -> Result<()> { + // TODO(anp): Remove after instruction-source helpers use target-native paths. + skip_if_wine_exec!(Ok(()), "requires native cross-OS instruction-source paths"); skip_if_no_network!(Ok(())); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index b417a38fc..294321910 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -51,6 +51,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::skip_if_remote; +use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::TestCodexBuilder; use core_test_support::test_codex::TestCodexHarness; use core_test_support::test_codex::local; @@ -911,6 +912,8 @@ async fn apply_patch_cli_preserves_existing_hard_link_outside_workspace() -> Res #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace() -> Result<()> { + // TODO(anp): Remove after apply-patch fixtures use target-native paths. + skip_if_wine_exec!(Ok(()), "asserts POSIX workspace traversal behavior"); skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; @@ -1566,6 +1569,11 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Result<()> { + // TODO(anp): Remove after shared-cwd helpers use target-native paths. + skip_if_wine_exec!( + Ok(()), + "requires a cwd valid in local POSIX and remote Windows environments" + ); skip_if_no_network!(Ok(())); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 670b0fa1a..829928002 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -38,6 +38,7 @@ use core_test_support::responses::ev_custom_tool_call; use core_test_support::responses::ev_response_created; use core_test_support::responses::sse; use core_test_support::skip_if_no_network; +use core_test_support::skip_if_wine_exec; use core_test_support::stdio_server_bin; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; @@ -972,6 +973,11 @@ text(result.output); #[cfg_attr(windows, ignore = "no exec_command on Windows")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn code_mode_exec_explicit_max_above_default_preserves_output() -> Result<()> { + // TODO(anp): Remove after Wine exec returns complete nested-tool output to code mode. + skip_if_wine_exec!( + Ok(()), + "only part of nested exec_command stdout reaches the code-mode result" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1002,6 +1008,11 @@ text(result.output); #[cfg_attr(windows, ignore = "no exec_command on Windows")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn code_mode_exec_explicit_max_above_default_truncates_larger_output() -> Result<()> { + // TODO(anp): Remove after Wine exec returns complete nested-tool output to code mode. + skip_if_wine_exec!( + Ok(()), + "only part of nested exec_command stdout reaches the code-mode result" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1036,6 +1047,11 @@ text(result.output); #[cfg_attr(windows, ignore = "no exec_command on Windows")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn code_mode_exec_explicit_max_above_truncation_policy_preserves_output() -> Result<()> { + // TODO(anp): Remove after Wine exec returns complete nested-tool output to code mode. + skip_if_wine_exec!( + Ok(()), + "only part of nested exec_command stdout reaches the code-mode result" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1069,6 +1085,11 @@ text(result.output); #[cfg_attr(windows, ignore = "no exec_command on Windows")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn code_mode_exec_without_max_preserves_output_beyond_default() -> Result<()> { + // TODO(anp): Remove after Wine exec returns complete nested-tool output to code mode. + skip_if_wine_exec!( + Ok(()), + "only part of nested exec_command stdout reaches the code-mode result" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1098,6 +1119,11 @@ text(result.output); #[cfg_attr(windows, ignore = "no exec_command on Windows")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn code_mode_exec_without_max_preserves_output_beyond_truncation_policy() -> Result<()> { + // TODO(anp): Remove after Wine exec returns complete nested-tool output to code mode. + skip_if_wine_exec!( + Ok(()), + "only part of nested exec_command stdout reaches the code-mode result" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; diff --git a/codex-rs/core/tests/suite/hierarchical_agents.rs b/codex-rs/core/tests/suite/hierarchical_agents.rs index c8c5da94b..f017d7d42 100644 --- a/codex-rs/core/tests/suite/hierarchical_agents.rs +++ b/codex-rs/core/tests/suite/hierarchical_agents.rs @@ -5,6 +5,7 @@ use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; +use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::test_codex; const HIERARCHICAL_AGENTS_SNIPPET: &str = @@ -12,6 +13,8 @@ const HIERARCHICAL_AGENTS_SNIPPET: &str = #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() { + // TODO(anp): Remove after instruction-source helpers use target-native paths. + skip_if_wine_exec!("requires native cross-OS instruction-source paths"); let server = start_mock_server().await; let resp_mock = mount_sse_once( &server, diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index 12aa3876a..d0a0916f3 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -31,6 +31,7 @@ use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; use core_test_support::PathBufExt; use core_test_support::PathExt; +use core_test_support::TestEnvironment; use core_test_support::get_remote_test_env; use core_test_support::responses::ev_apply_patch_custom_tool_call; use core_test_support::responses::ev_assistant_message; @@ -42,6 +43,7 @@ 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::skip_if_no_network; +use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::local; use core_test_support::test_codex::test_codex; @@ -156,7 +158,7 @@ async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> { let test_env = test_env().await?; let file_system = test_env.environment().get_filesystem(); - let file_path_abs = remote_test_file_path().abs(); + let file_path_abs = test_env.cwd().join("remote-test-env-ok"); let file_path_uri = PathUri::from_path(&file_path_abs)?; let payload = b"remote-test-env-ok".to_vec(); @@ -183,7 +185,7 @@ async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn remote_test_env_exposes_bash_shell_to_model() -> Result<()> { +async fn remote_test_env_exposes_target_shell_to_model() -> Result<()> { let Some(_remote_env) = get_remote_test_env() else { return Ok(()); }; @@ -208,11 +210,19 @@ async fn remote_test_env_exposes_bash_shell_to_model() -> Result<()> { .into_iter() .find(|text| text.starts_with("")) .context("environment context should be model visible")?; + // TODO(anp): Assert Wine-exec exposes a `C:\\...` cwd after model-visible paths preserve + // target-native spelling instead of the Linux orchestrator's `/C:/...` representation. + let expected_shell = match core_test_support::test_environment() { + TestEnvironment::Docker { .. } => "bash", + TestEnvironment::WineExec => "powershell", + TestEnvironment::Local => unreachable!("test requires a remote environment"), + }; assert_eq!( environment_context .lines() - .find(|line| line.trim_start().starts_with("")), - Some(" bash"), + .find(|line| line.trim_start().starts_with("")) + .map(str::trim), + Some(expected_shell), ); Ok(()) @@ -272,8 +282,11 @@ fn assert_normalized_path_rejected(error: &std::io::Error) { fn remote_exec(script: &str) -> Result<()> { let remote_env = get_remote_test_env().context("remote env should be configured")?; + let container_name = remote_env + .docker_container_name() + .context("test requires direct access to the Docker container")?; let output = Command::new("docker") - .args(["exec", &remote_env.container_name, "sh", "-lc", script]) + .args(["exec", container_name, "sh", "-lc", script]) .output()?; assert!( output.status.success(), @@ -319,6 +332,8 @@ async fn exec_command_routing_output( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn exec_command_routes_to_selected_remote_environment() -> Result<()> { skip_if_no_network!(Ok(())); + // TODO(anp): Remove after remote path fixtures use target-native paths. + skip_if_wine_exec!(Ok(()), "requires the Docker-backed POSIX executor"); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); }; @@ -394,6 +409,8 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result<()> { skip_if_no_network!(Ok(())); + // TODO(anp): Remove after remote path fixtures use target-native paths. + skip_if_wine_exec!(Ok(()), "requires the Docker-backed POSIX executor"); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); }; @@ -598,6 +615,8 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result<()> { skip_if_no_network!(Ok(())); + // TODO(anp): Remove after remote path fixtures use target-native paths. + skip_if_wine_exec!(Ok(()), "requires the Docker-backed POSIX executor"); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); }; @@ -684,6 +703,8 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result< #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> { skip_if_no_network!(Ok(())); + // TODO(anp): Remove after remote path fixtures use target-native paths. + skip_if_wine_exec!(Ok(()), "requires the Docker-backed POSIX executor"); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); }; @@ -870,6 +891,8 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> { async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environment() -> Result<()> { skip_if_no_network!(Ok(())); + // TODO(anp): Remove after remote path fixtures use target-native paths. + skip_if_wine_exec!(Ok(()), "requires the Docker-backed POSIX executor"); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); }; @@ -964,6 +987,8 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_test_env_sandboxed_read_allows_readable_root() -> Result<()> { + // TODO(anp): Remove after remote sandbox fixtures use target-native paths. + skip_if_wine_exec!(Ok(()), "requires the Docker-backed POSIX executor"); skip_if_no_network!(Ok(())); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); @@ -1013,6 +1038,7 @@ async fn remote_test_env_sandboxed_read_allows_readable_root() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape() -> Result<()> { + skip_if_wine_exec!(Ok(()), "tests POSIX symlink and parent traversal semantics"); skip_if_no_network!(Ok(())); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); @@ -1048,6 +1074,7 @@ async fn remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape() - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_test_env_remove_removes_symlink_not_target() -> Result<()> { + skip_if_wine_exec!(Ok(()), "tests POSIX symlink removal semantics"); skip_if_no_network!(Ok(())); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); @@ -1118,6 +1145,7 @@ async fn remote_test_env_remove_removes_symlink_not_target() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_test_env_copy_preserves_symlink_source() -> Result<()> { + skip_if_wine_exec!(Ok(()), "tests POSIX symlink copy semantics"); skip_if_no_network!(Ok(())); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); @@ -1153,12 +1181,14 @@ async fn remote_test_env_copy_preserves_symlink_source() -> Result<()> { ) .await?; + let remote_env = get_remote_test_env().context("remote env should be configured")?; + let container_name = remote_env + .docker_container_name() + .context("test requires direct access to the Docker container")?; let link_target = Command::new("docker") .args([ "exec", - &get_remote_test_env() - .context("remote env should still be configured")? - .container_name, + container_name, "readlink", copied_symlink .to_str() @@ -1188,14 +1218,3 @@ async fn remote_test_env_copy_preserves_symlink_source() -> Result<()> { .await?; Ok(()) } - -fn remote_test_file_path() -> PathBuf { - let nanos = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(duration) => duration.as_nanos(), - Err(_) => 0, - }; - PathBuf::from(format!( - "/tmp/codex-remote-test-env-{}-{nanos}.txt", - std::process::id() - )) -} diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 961c66079..0c8cd8cb5 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -49,15 +49,16 @@ use codex_protocol::user_input::UserInput; use codex_utils_cargo_bin::cargo_bin; use codex_utils_path_uri::PathUri; use core_test_support::assert_regex_match; -use core_test_support::remote_env_env_var; use core_test_support::responses; use core_test_support::responses::mount_models_once; use core_test_support::responses::mount_sse_once; use core_test_support::skip_if_no_network; +use core_test_support::skip_if_wine_exec; use core_test_support::stdio_server_bin; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; +use core_test_support::test_environment; use core_test_support::wait_for_event; use core_test_support::wait_for_mcp_server; use image::DynamicImage; @@ -166,12 +167,11 @@ enum McpCallEvent { const REMOTE_MCP_ENVIRONMENT: &str = "remote"; fn remote_aware_environment_id() -> String { - // These tests run locally in normal CI and against the Docker-backed - // executor in full-ci. Match that shared test environment instead of - // parameterizing each stdio MCP test with its own local/remote cases. - std::env::var_os(remote_env_env_var()) - .map(|_| REMOTE_MCP_ENVIRONMENT.to_string()) - .unwrap_or_else(|| codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string()) + if test_environment().is_remote() { + REMOTE_MCP_ENVIRONMENT.to_string() + } else { + codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string() + } } /// Returns the stdio MCP test server command path for the active test placement. @@ -183,7 +183,8 @@ fn remote_aware_environment_id() -> String { /// container and return that in-container path instead. fn remote_aware_stdio_server_bin() -> anyhow::Result { let bin = stdio_server_bin()?; - let Some(container_name) = remote_env_container_name()? else { + let environment = test_environment(); + let Some(container_name) = environment.docker_container_name() else { return Ok(bin); }; @@ -196,17 +197,7 @@ fn remote_aware_stdio_server_bin() -> anyhow::Result { // path instead of the host build artifact path. // Several remote-aware MCP tests can run in parallel; give each copied // binary its own path so one test cannot replace another test's executable. - copy_binary_to_remote_env(&container_name, Path::new(&bin), "test_stdio_server") -} - -/// Returns the Docker container used by remote-aware MCP tests, when active. -fn remote_env_container_name() -> anyhow::Result> { - let Some(container_name) = std::env::var_os(remote_env_env_var()) else { - return Ok(None); - }; - Ok(Some(container_name.into_string().map_err(|value| { - anyhow::anyhow!("remote env container name must be utf-8: {value:?}") - })?)) + copy_binary_to_remote_env(container_name, Path::new(&bin), "test_stdio_server") } /// Builds a collision-resistant in-container path for copied test binaries. @@ -406,7 +397,7 @@ fn assert_cwd_tool_output(structured: &Value, expected_cwd: &Path) { .and_then(Value::as_str) .expect("cwd tool should return a string cwd"); - if std::env::var_os(remote_env_env_var()).is_some() { + if test_environment().is_remote() { assert_eq!( structured, &json!({ @@ -431,6 +422,11 @@ fn assert_cwd_tool_output(structured: &Value, expected_cwd: &Path) { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_test_value)] async fn stdio_server_round_trip() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -614,6 +610,11 @@ async fn shutdown_cancels_startup_prewarm_waiting_for_mcp_startup() -> anyhow::R #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_cwd)] async fn stdio_server_uses_configured_cwd_before_runtime_fallback() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -739,6 +740,11 @@ async fn local_stdio_server_uses_runtime_fallback_cwd_when_config_omits_cwd() -> #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -832,6 +838,11 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()> #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -947,6 +958,11 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow:: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn stdio_mcp_read_only_tool_calls_run_concurrently_without_server_opt_in() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1044,6 +1060,11 @@ async fn stdio_mcp_read_only_tool_calls_run_concurrently_without_server_opt_in() #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1132,6 +1153,11 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_test_value)] async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1269,6 +1295,11 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_test_value)] async fn stdio_image_responses_resize_large_image() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1370,6 +1401,11 @@ async fn stdio_image_responses_resize_large_image() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_test_value)] async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1456,6 +1492,11 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_test_value)] async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1609,6 +1650,11 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_test_value)] async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1726,6 +1772,11 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_env_source)] async fn stdio_server_propagates_explicit_local_env_var_source() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1817,8 +1868,13 @@ async fn stdio_server_propagates_explicit_local_env_var_source() -> anyhow::Resu #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_env_source)] async fn remote_stdio_env_var_source_does_not_copy_local_env() -> anyhow::Result<()> { + // TODO(anp): Remove after packaging a Windows stdio test server for Wine exec. + skip_if_wine_exec!( + Ok(()), + "requires a Windows test_stdio_server in the Wine-exec environment" + ); skip_if_no_network!(Ok(())); - if std::env::var_os(remote_env_env_var()).is_none() { + if !test_environment().is_remote() { return Ok(()); } @@ -2334,10 +2390,11 @@ async fn start_streamable_http_test_server( } }; - if let Some(container_name) = remote_env_container_name()? { + let environment = test_environment(); + if let Some(container_name) = environment.docker_container_name() { return Ok(Some( start_remote_streamable_http_test_server( - &container_name, + container_name, &rmcp_http_server_bin, expected_env_value, expected_token, diff --git a/codex-rs/core/tests/suite/shell_serialization.rs b/codex-rs/core/tests/suite/shell_serialization.rs index 2295798da..77a6d153a 100644 --- a/codex-rs/core/tests/suite/shell_serialization.rs +++ b/codex-rs/core/tests/suite/shell_serialization.rs @@ -12,6 +12,7 @@ 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::skip_if_no_network; +use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::test_codex; use pretty_assertions::assert_eq; use regex_lite::Regex; @@ -235,6 +236,8 @@ M {file_name} #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn apply_patch_custom_tool_call_reports_failure_output() -> Result<()> { + // TODO(anp): Remove after apply-patch assertions use target-native paths. + skip_if_wine_exec!(Ok(()), "asserts POSIX apply_patch failure text"); skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index 55c75016d..adc0b5ccc 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -17,6 +17,7 @@ use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; +use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; @@ -47,6 +48,8 @@ async fn write_repo_skill( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn user_turn_includes_skill_instructions() -> Result<()> { + // TODO(anp): Remove after skill-path helpers use target-native paths. + skip_if_wine_exec!(Ok(()), "requires native cross-OS skill paths"); skip_if_no_network!(Ok(())); let server = start_mock_server().await; diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index ac1aac0b1..500b8cb75 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -34,6 +34,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; use core_test_support::skip_if_windows; +use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::TestCodexHarness; use core_test_support::test_codex::test_codex; @@ -384,6 +385,11 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_emits_exec_command_begin_event() -> Result<()> { + // TODO(anp): Remove after unified-exec fixtures use target-native commands. + skip_if_wine_exec!( + Ok(()), + "uses a POSIX command and does not assert successful execution" + ); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -443,6 +449,11 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_resolves_relative_workdir() -> Result<()> { + // TODO(anp): Remove after workdir helpers use target-native paths. + skip_if_wine_exec!( + Ok(()), + "does not assert successful native-Windows workdir execution" + ); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -577,6 +588,8 @@ async fn unified_exec_respects_workdir_override() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_emits_exec_command_end_event() -> Result<()> { + // TODO(anp): Remove after unified-exec fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses a POSIX-only command fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -650,6 +663,8 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> { + // TODO(anp): Remove after unified-exec fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses a POSIX-only command fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -708,6 +723,8 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> { + // TODO(anp): Remove after unified-exec fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses a POSIX-only command fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -803,6 +820,8 @@ async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_network_denial_emits_failed_background_end_event() -> Result<()> { + // TODO(anp): Remove after network-denial fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses the POSIX/Python network-denial fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -846,6 +865,8 @@ async fn unified_exec_network_denial_emits_failed_background_end_event() -> Resu #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_short_lived_network_denial_emits_failed_end_event() -> Result<()> { + // TODO(anp): Remove after network-denial fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses the POSIX/Python network-denial fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -999,6 +1020,8 @@ async fn wait_for_unified_exec_end( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()> { + // TODO(anp): Remove after unified-exec interactive fixtures support Windows/ConPTY. + skip_if_wine_exec!(Ok(()), "uses POSIX interactive-process and EOF semantics"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -1082,6 +1105,8 @@ async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()> #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<()> { + // TODO(anp): Remove after timing fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses a POSIX sleep/echo timing fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -1384,6 +1409,8 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> { + // TODO(anp): Remove after unified-exec fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses a POSIX-only command fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -1477,6 +1504,8 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn exec_command_clamps_model_requested_max_output_tokens_to_policy() -> Result<()> { + // TODO(anp): Remove after unified-exec fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses a POSIX-only command fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -1539,6 +1568,8 @@ async fn exec_command_clamps_model_requested_max_output_tokens_to_policy() -> Re #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn write_stdin_clamps_model_requested_max_output_tokens_to_policy() -> Result<()> { + // TODO(anp): Remove after unified-exec interactive fixtures support Windows/ConPTY. + skip_if_wine_exec!(Ok(()), "uses POSIX read/while and Unix TTY semantics"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -1628,6 +1659,8 @@ async fn write_stdin_clamps_model_requested_max_output_tokens_to_policy() -> Res #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_defaults_to_pipe() -> Result<()> { + // TODO(anp): Remove after unified-exec interactive fixtures support Windows/ConPTY. + skip_if_wine_exec!(Ok(()), "requires Python/Unix PTY support in the target"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -1697,6 +1730,8 @@ async fn unified_exec_defaults_to_pipe() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_can_enable_tty() -> Result<()> { + // TODO(anp): Remove after unified-exec interactive fixtures support Windows/ConPTY. + skip_if_wine_exec!(Ok(()), "requires Python/Unix PTY support in the target"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -1763,6 +1798,8 @@ async fn unified_exec_can_enable_tty() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_respects_early_exit_notifications() -> Result<()> { + // TODO(anp): Remove after unified-exec fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses a POSIX-only command fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -1846,6 +1883,8 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> { + // TODO(anp): Remove after unified-exec interactive fixtures support Windows/ConPTY. + skip_if_wine_exec!(Ok(()), "uses POSIX interactive-process and EOF semantics"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -1999,6 +2038,8 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn write_stdin_ctrl_c_interrupts_non_tty_session() -> Result<()> { + // TODO(anp): Add a Wine-exec test for explicit interrupt handling on Windows. + skip_if_wine_exec!(Ok(()), "asserts Unix SIGINT and trap semantics"); assert_write_stdin_ctrl_c_interrupts_non_tty_session( "trap", "trap 'echo INT-TRAP; exit 42' INT; echo READY; while true; do sleep 30; done", @@ -2010,6 +2051,8 @@ async fn write_stdin_ctrl_c_interrupts_non_tty_session() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn write_stdin_ctrl_c_default_interrupt_reports_130_for_non_tty_session() -> Result<()> { + // TODO(anp): Add a Wine-exec test for Windows Ctrl+C termination and exit reporting. + skip_if_wine_exec!(Ok(()), "asserts Unix SIGINT and exit-code semantics"); assert_write_stdin_ctrl_c_interrupts_non_tty_session( "default", "echo READY; exec sleep 30", @@ -2242,6 +2285,8 @@ async fn write_stdin_ctrl_c_reports_unsupported_interrupt_to_model_on_windows() #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<()> { + // TODO(anp): Remove after unified-exec interactive fixtures support Windows/ConPTY. + skip_if_wine_exec!(Ok(()), "uses POSIX interactive-process and EOF semantics"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -2542,6 +2587,8 @@ async fn unified_exec_interrupt_preserves_long_running_session() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_reuses_session_via_stdin() -> Result<()> { + // TODO(anp): Remove after unified-exec interactive fixtures support Windows/ConPTY. + skip_if_wine_exec!(Ok(()), "uses POSIX interactive-process and EOF semantics"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -2640,6 +2687,8 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_streams_after_lagged_output() -> Result<()> { + // TODO(anp): Remove after output fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "requires Python/Unix PTY support in the target"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -2756,6 +2805,8 @@ PY #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_timeout_and_followup_poll() -> Result<()> { + // TODO(anp): Remove after unified-exec fixtures use target-native commands. + skip_if_wine_exec!(Ok(()), "uses a POSIX-only command fixture"); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -2845,6 +2896,11 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> { // Skipped on arm because the ctor logic to handle arg0 doesn't work on ARM #[cfg(not(target_arch = "arm"))] async fn unified_exec_formats_large_output_summary() -> Result<()> { + // TODO(anp): Remove after output fixtures use target-native commands. + skip_if_wine_exec!( + Ok(()), + "requires Python and POSIX heredoc support in the target" + ); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); skip_if_windows!(Ok(())); @@ -3288,6 +3344,11 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_runs_on_all_platforms() -> Result<()> { + // TODO(anp): Remove after PowerShell execution passes through Wine exec. + skip_if_wine_exec!( + Ok(()), + "basic PowerShell execution through Wine exec is not passing yet" + ); skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index bbce61084..1857eedef 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -43,6 +43,7 @@ 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::skip_if_no_network; +use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::local; use core_test_support::test_codex::test_codex; @@ -592,6 +593,8 @@ async fn view_image_tool_applies_local_sandbox_read_denies() -> anyhow::Result<( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<()> { + // TODO(anp): Remove after remote-cwd fixtures use target-native paths. + skip_if_wine_exec!(Ok(()), "hardcodes a POSIX remote cwd"); skip_if_no_network!(Ok(())); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); diff --git a/codex-rs/exec-server/testing/BUILD.bazel b/codex-rs/exec-server/testing/BUILD.bazel index bf50e0bce..ac4818f55 100644 --- a/codex-rs/exec-server/testing/BUILD.bazel +++ b/codex-rs/exec-server/testing/BUILD.bazel @@ -16,6 +16,21 @@ rust_library( ], ) +rust_binary( + name = "wine-exec-test-runner", + testonly = True, + srcs = ["wine_remote_test_runner.rs"], + crate_name = "wine_exec_test_runner", + crate_root = "wine_remote_test_runner.rs", + target_compatible_with = ["@platforms//os:linux"], + visibility = ["//visibility:public"], + deps = [ + ":wine-exec-server-test-support", + "@crates//:anyhow", + "@crates//:tokio", + ], +) + rust_binary( name = "windows-exec-server", testonly = True, @@ -24,7 +39,7 @@ rust_binary( crate_root = "windows_exec_server.rs", tags = ["manual"], target_compatible_with = ["@platforms//os:windows"], - visibility = ["//codex-rs/core/tests/remote_env_windows:__pkg__"], + visibility = ["//visibility:public"], deps = [ "//codex-rs/exec-server", "@crates//:tokio", diff --git a/codex-rs/exec-server/testing/wine_remote_test_runner.rs b/codex-rs/exec-server/testing/wine_remote_test_runner.rs new file mode 100644 index 000000000..288941567 --- /dev/null +++ b/codex-rs/exec-server/testing/wine_remote_test_runner.rs @@ -0,0 +1,62 @@ +use std::ffi::OsStr; +use std::ffi::OsString; +use std::path::PathBuf; +use std::process::Stdio; + +use anyhow::Context; +use anyhow::Result; +use tokio::process::Command; +use wine_exec_server_test_support::WineExecServer; + +const TEST_BINARY_ENV_VAR: &str = "CODEX_WINE_EXEC_TEST_BINARY"; +const TEST_ENVIRONMENT_ENV_VAR: &str = "CODEX_TEST_ENVIRONMENT"; +const REMOTE_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_TEST_REMOTE_EXEC_SERVER_URL"; +const LEGACY_REMOTE_ENV_ENV_VAR: &str = "CODEX_TEST_REMOTE_ENV"; +const DOCKER_CONTAINER_ENV_VAR: &str = "CODEX_TEST_REMOTE_ENV_CONTAINER_NAME"; + +#[tokio::main(flavor = "multi_thread", worker_threads = 2)] +async fn main() -> Result<()> { + let test_binary = PathBuf::from( + std::env::var_os(TEST_BINARY_ENV_VAR) + .with_context(|| format!("{TEST_BINARY_ENV_VAR} must be set by the Bazel test rule"))?, + ); + let forwarded_args = std::env::args_os().skip(1).collect::>(); + + if is_terse_list_request(&forwarded_args) { + let status = Command::new(&test_binary) + .args(&forwarded_args) + .status() + .await + .context("list integration tests")?; + anyhow::ensure!(status.success(), "listing integration tests exited with {status}"); + return Ok(()); + } + + WineExecServer + .scope(|exec_server_url| async move { + let mut command = Command::new(test_binary); + command + .env(TEST_ENVIRONMENT_ENV_VAR, "wine-exec") + .env(REMOTE_EXEC_SERVER_URL_ENV_VAR, exec_server_url) + .env_remove(LEGACY_REMOTE_ENV_ENV_VAR) + .env_remove(DOCKER_CONTAINER_ENV_VAR) + .args(forwarded_args) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .kill_on_drop(true); + + let status = command.status().await.context("run integration tests")?; + anyhow::ensure!(status.success(), "integration tests exited with {status}"); + Ok(()) + }) + .await +} + +fn is_terse_list_request(args: &[OsString]) -> bool { + args.iter().map(OsString::as_os_str).eq([ + OsStr::new("--list"), + OsStr::new("--format"), + OsStr::new("terse"), + ]) +} diff --git a/defs.bzl b/defs.bzl index 365f4512f..d49b06c26 100644 --- a/defs.bzl +++ b/defs.bzl @@ -2,6 +2,8 @@ load("@crates//:data.bzl", "DEP_DATA") load("@crates//:defs.bzl", "all_crate_deps") load("@rules_rust//cargo/private:cargo_build_script_wrapper.bzl", "cargo_build_script") load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_proc_macro", "rust_test") +load("//bazel/rules/testing:foreign_platform_binary.bzl", "foreign_platform_binary") +load("//bazel/rules/testing/wine:wine_runtime.bzl", "WINE_TEST_TARGET_COMPATIBLE_WITH", "wine_test_runtime") # Match Cargo's Windows linker behavior so Bazel-built binaries and tests use # the same stack reserve on both Windows ABIs and resolve UCRT imports on MSVC. @@ -70,19 +72,19 @@ def _workspace_root_test_impl(ctx): runfiles = runfiles.merge(ctx.runfiles(files = data_dep[DefaultInfo].files.to_list())) runfiles = runfiles.merge(data_dep[DefaultInfo].default_runfiles) for runfile_dep in ctx.attr.runfile_env: - executable = runfile_dep[DefaultInfo].files_to_run.executable - if executable == None: - fail("{} does not provide an executable for runfile_env".format(runfile_dep.label)) - runfiles = runfiles.merge(ctx.runfiles(files = [executable])) + runfile = _runfile_env_file(runfile_dep) + runfiles = runfiles.merge(ctx.runfiles(files = [runfile])) runfiles = runfiles.merge(runfile_dep[DefaultInfo].default_runfiles) - location_targets = ( + location_targets = {} + for target in ( ctx.attr.data + [ctx.attr.test_bin, ctx.attr.workspace_root_marker] + ctx.attr.runfile_env.keys() - ) + ): + location_targets[target.label] = target env = { - key: ctx.expand_location(value, targets = location_targets) + key: ctx.expand_location(value, targets = location_targets.values()) for key, value in ctx.attr.env.items() } @@ -100,22 +102,31 @@ def _workspace_root_test_impl(ctx): def _bash_runfile_env_exports(ctx): lines = [] for runfile_dep, env_var in ctx.attr.runfile_env.items(): - executable = runfile_dep[DefaultInfo].files_to_run.executable - if executable == None: - fail("{} does not provide an executable for runfile_env".format(runfile_dep.label)) - lines.append('RUNFILE_ENV_ARGS+=("{}=$(resolve_runfile "{}")")'.format(env_var, executable.short_path)) + runfile = _runfile_env_file(runfile_dep) + lines.append('RUNFILE_ENV_ARGS+=("{}=$(resolve_runfile "{}")")'.format(env_var, _runfile_logical_path(runfile))) return "\n".join(lines) def _windows_runfile_env_exports(ctx): lines = [] for runfile_dep, env_var in ctx.attr.runfile_env.items(): - executable = runfile_dep[DefaultInfo].files_to_run.executable - if executable == None: - fail("{} does not provide an executable for runfile_env".format(runfile_dep.label)) - lines.append('call :resolve_runfile {} "{}"'.format(env_var, executable.short_path)) + runfile = _runfile_env_file(runfile_dep) + lines.append('call :resolve_runfile {} "{}"'.format(env_var, _runfile_logical_path(runfile))) lines.append("if errorlevel 1 exit /b 1") return "\n".join(lines) +def _runfile_env_file(target): + executable = target[DefaultInfo].files_to_run.executable + if executable != None: + return executable + + files = target[DefaultInfo].files.to_list() + if len(files) != 1: + fail("{} must provide an executable or exactly one file for runfile_env".format(target.label)) + return files[0] + +def _runfile_logical_path(file): + return file.short_path.removeprefix("../") + def _bash_workspace_root_setup(ctx): if not ctx.attr.chdir_workspace_root: return "" @@ -188,7 +199,8 @@ def codex_rust_crate( test_tags = [], unit_test_timeout = None, extra_binaries = [], - extra_binaries_non_windows = []): + extra_binaries_non_windows = [], + run_tests_with_wine_exec = False): """Defines a Rust crate with library, binaries, and tests wired for Bazel + Cargo parity. The macro mirrors Cargo conventions: it builds a library when `src/` exists, @@ -236,6 +248,9 @@ def codex_rust_crate( extra_binaries_non_windows: Like `extra_binaries`, but omitted from Windows test data and environment variables. Tests using these binaries must be excluded when targeting Windows. + run_tests_with_wine_exec: Boolean, defaults to False. Whether to emit a + Wine-exec variant for each integration test. Variants inherit the + native test's timeout, tags, and shard count. """ test_env = { # The launcher resolves an absolute workspace root at runtime so @@ -377,10 +392,10 @@ def codex_rust_crate( integration_test_binaries = sanitized_binaries integration_test_cargo_env = cargo_env integration_test_cargo_env_runfiles = cargo_env_runfiles + non_windows_sanitized_binaries = [] + non_windows_cargo_env = {} + non_windows_cargo_env_runfiles = {} if extra_binaries_non_windows: - non_windows_sanitized_binaries = [] - non_windows_cargo_env = {} - non_windows_cargo_env_runfiles = {} for binary_label in extra_binaries_non_windows: non_windows_sanitized_binaries.append(binary_label) binary = Label(binary_label).name @@ -400,6 +415,11 @@ def codex_rust_crate( "//conditions:default": cargo_env_runfiles | non_windows_cargo_env_runfiles, }) + wine_host_binaries = { + env_var.removeprefix("CARGO_BIN_EXE_"): binary_label + for binary_label, env_var in (cargo_env_runfiles | non_windows_cargo_env_runfiles).items() + } + integration_test_kwargs = {} if integration_test_args: integration_test_kwargs["args"] = integration_test_args @@ -428,7 +448,7 @@ def codex_rust_crate( integration_test_binary = test_name + "-bin" - # There are three generated integration-test shapes: + # There are four generated integration-test shapes: # # 1. Unsharded native tests keep the plain rust_test label for minimal # churn and the usual rules_rust Cargo-like environment. @@ -439,6 +459,12 @@ def codex_rust_crate( # 3. Windows cross tests always use the workspace_root_test wrapper so # runfile env vars become Windows-native absolute paths before the # Rust process starts. + # 4. Wine-exec tests reuse the native Rust test binary behind a shared + # Linux runner. The runner starts the cross-built Windows exec server + # under pinned Wine, injects its URL into the test environment, and + # owns cleanup. The outer workspace_root_test resolves the runner, + # test, and server from runfiles, sets a Cargo-like cwd, and applies + # the native test's shard count. if test_shard_count: # This target is intentionally a binary-like helper, not the public # test target. The wrapper below owns cwd setup, runfile env @@ -504,6 +530,51 @@ def codex_rust_crate( **test_kwargs ) + if run_tests_with_wine_exec: + wine_test_name = test_name.removesuffix("-test") + "-wine-exec-test" + native_test_binary = ":" + (integration_test_binary if test_shard_count else test_name) + wine_test_binaries = dict(wine_host_binaries) + + wine_exec_server = wine_test_name + "-windows-exec-server" + foreign_platform_binary( + name = wine_exec_server, + binary = "//codex-rs/exec-server/testing:windows-exec-server", + extra_rustc_flags = WINDOWS_GNULLVM_RUSTC_LINK_FLAGS, + platform = "//:windows_x86_64_gnullvm", + tags = ["manual"], + target_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + testonly = True, + visibility = ["//visibility:private"], + ) + wine_test_binaries["wine-windows-exec-server"] = ":" + wine_exec_server + wine_runtime = wine_test_runtime(wine_test_binaries) + wine_runfile_env = dict(wine_runtime.runfile_env) + wine_runfile_env[native_test_binary] = "CODEX_WINE_EXEC_TEST_BINARY" + + wine_test_kwargs = {} + wine_test_kwargs.update(integration_test_kwargs) + if test_shard_count: + wine_test_kwargs["shard_count"] = test_shard_count + wine_test_kwargs["flaky"] = True + + # The Wine runner is a binary rather than a rust_test, but it still + # needs a Cargo-like cwd, Bazel sharding, and absolute runfile paths. + # `workspace_root_test` establishes all three before Wine starts. + workspace_root_test( + name = wine_test_name, + data = wine_runtime.data, + env = test_env, + runfile_env = wine_runfile_env, + test_bin = "//codex-rs/exec-server/testing:wine-exec-test-runner", + workspace_root_marker = "//codex-rs/utils/cargo-bin:repo_root.marker", + target_compatible_with = WINE_TEST_TARGET_COMPATIBLE_WITH, + tags = test_tags + ["manual"], + **wine_test_kwargs + ) + windows_cross_test_kwargs = {} windows_cross_test_kwargs.update(integration_test_kwargs) if test_shard_count: diff --git a/scripts/test-remote-env.sh b/scripts/test-remote-env.sh index 584a0f6f2..aac888e7d 100755 --- a/scripts/test-remote-env.sh +++ b/scripts/test-remote-env.sh @@ -89,6 +89,8 @@ setup_remote_env() { fi export CODEX_TEST_REMOTE_ENV="${container_name}" + export CODEX_TEST_REMOTE_ENV_CONTAINER_NAME="${container_name}" + export CODEX_TEST_ENVIRONMENT="docker" } wait_for_remote_exec_server_port() { @@ -114,8 +116,10 @@ codex_remote_env_cleanup() { docker rm -f "${CODEX_TEST_REMOTE_ENV}" >/dev/null 2>&1 || true unset CODEX_TEST_REMOTE_ENV fi + unset CODEX_TEST_REMOTE_ENV_CONTAINER_NAME unset CODEX_TEST_REMOTE_EXEC_SERVER_PID unset CODEX_TEST_REMOTE_EXEC_SERVER_URL + unset CODEX_TEST_ENVIRONMENT } if ! is_sourced; then @@ -128,6 +132,7 @@ set -euo pipefail if setup_remote_env; then status=0 echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" + echo "CODEX_TEST_ENVIRONMENT=${CODEX_TEST_ENVIRONMENT}" 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