[codex] Add hermetic Wine exec-server test (#27937)

## Why

We want to make it possible for an app-server orchestrator on one OS to
control an exec-server on another host running a different OS. In
practice this kinda already works if you get lucky and the two hosts
have the same path format, but we mangle quite a lot of operations if
either end is Windows.

This test starts exercising that interaction, although right now the
initial bootstrap fails. Future changes will expand the test's
assertions to match improved support.

## What

Stacked on #27964. This adds a small Windows exec-server fixture and a
Linux protocol smoke test using the reusable Wine harness, covering
Windows environment discovery, non-TTY `cmd.exe` execution, output, exit
status, and working directory.

Once we've got the full codex binary cross-building under Bazel we could
consider moving to the real binary instead of the stripped down
exec-server-only binary used here.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-12 20:20:23 -07:00
committed by GitHub
Unverified
parent b9dc3b7a8b
commit 9d938a46d9
10 changed files with 312 additions and 3 deletions
+10 -2
View File
@@ -13,6 +13,7 @@ _WINE_RUNTIME_BINARIES = {
def wine_rust_test(
name,
windows_binaries,
host_binaries = {},
data = [],
target_compatible_with = [],
**kwargs):
@@ -22,7 +23,8 @@ def wine_rust_test(
every Rust dependency receives the repository's Windows linker flags while
the test stays on x86-64 Linux. Its environment-variable contract is:
* Each entry contributes `CARGO_BIN_EXE_<binary_name>` for its executable.
* Each `host_binaries` and `windows_binaries` entry contributes
`CARGO_BIN_EXE_<binary_name>` for its executable.
* `CARGO_BIN_EXE_wine` and `CARGO_BIN_EXE_wineserver` identify Wine tools.
* `CARGO_BIN_EXE_wine-runtime-marker` identifies a file whose parent is the
Wine DLL directory to use as `WINEDLLPATH`.
@@ -34,14 +36,20 @@ def wine_rust_test(
Args:
name: Name of the generated Linux `rust_test`.
windows_binaries: Map from `CARGO_BIN_EXE_*` suffixes to Windows targets.
host_binaries: Map from `CARGO_BIN_EXE_*` suffixes to Linux host targets.
data: Additional runtime data for the Linux test.
target_compatible_with: Additional compatibility constraints.
**kwargs: Remaining attributes forwarded to `rust_test`.
"""
binaries = dict(_WINE_RUNTIME_BINARIES)
for binary_name in sorted(host_binaries.keys()):
if binary_name in binaries:
fail("host test binary name collides with Wine runtime: {}".format(binary_name))
binaries[binary_name] = host_binaries[binary_name]
for index, binary_name in enumerate(sorted(windows_binaries.keys())):
if binary_name in binaries:
fail("Windows test binary name collides with Wine runtime: {}".format(binary_name))
fail("Windows test binary name collides with existing binary: {}".format(binary_name))
transitioned_binary = name + "-windows-binary-" + str(index)
foreign_platform_binary(
name = transitioned_binary,
+1
View File
@@ -163,3 +163,4 @@ zstd = { workspace = true }
[package.metadata.cargo-shear]
ignored = ["openssl-sys"]
ignored-paths = ["tests/remote_env_windows/*.rs"]
@@ -0,0 +1,24 @@
load("//bazel/rules/testing:wine.bzl", "wine_rust_test")
wine_rust_test(
name = "smoke-test",
timeout = "short",
srcs = ["remote_env_windows_test.rs"],
crate_name = "remote_env_windows_test",
crate_root = "remote_env_windows_test.rs",
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",
"@crates//:anyhow",
"@crates//:pretty_assertions",
"@crates//:serde_json",
"@crates//:tokio",
],
)
@@ -0,0 +1,24 @@
# Windows remote-environment test
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.
## Running the test
```sh
bazel test \
//codex-rs/core/tests/remote_env_windows:smoke-test \
--test_output=errors
```
No system Wine is required. Every process gets a fresh `WINEPREFIX` and isolated
wineserver.
## Current limitations
- PowerShell and ConPTY/TTY behavior are 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.
@@ -0,0 +1,207 @@
//! Bazel-only integration coverage for a Windows exec-server running under Wine.
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 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;
const CALL_ID: &str = "wine-cmd-smoke";
const COMMAND: &str = "echo WINE_BAZEL_OK&&cd";
#[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 environments = TurnEnvironmentSelections::new(
test.config.cwd.clone(),
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: test.config.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),
(
test.config.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,
test.config.cwd.clone(),
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
}
+16
View File
@@ -0,0 +1,16 @@
load("@rules_rust//rust:defs.bzl", "rust_binary")
rust_binary(
name = "windows-exec-server",
testonly = True,
srcs = ["windows_exec_server.rs"],
crate_name = "windows_exec_server",
crate_root = "windows_exec_server.rs",
tags = ["manual"],
target_compatible_with = ["@platforms//os:windows"],
visibility = ["//codex-rs/core/tests/remote_env_windows:__pkg__"],
deps = [
"//codex-rs/exec-server",
"@crates//:tokio",
],
)
+5
View File
@@ -0,0 +1,5 @@
# Windows exec-server fixture
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.
@@ -0,0 +1,18 @@
//! Minimal Windows exec-server fixture for cross-platform tests.
//!
//! Keeping this wrapper separate avoids depending on the full Codex binary's
//! Windows cross-build, which is not yet supported by the Bazel graph. Linking
//! only the exec-server also makes the Wine test substantially faster to
//! iterate on.
use codex_exec_server::ExecServerRuntimePaths;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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.
let runtime_paths =
ExecServerRuntimePaths::new(current_exe, /*codex_linux_sandbox_exe*/ None)?;
codex_exec_server::run_main("ws://127.0.0.1:0", runtime_paths).await
}
+3
View File
@@ -11,6 +11,9 @@ cd "${repo_root}"
# Exclude the experimental `v8-poc` target because it pulls in expensive V8
# build machinery that is unrelated to the release-only Rust regression this
# workflow is meant to catch.
# The normal test job covers the Wine smoke test; omit its downloaded runtime
# and cross-compile from this build-only release sweep.
printf '%s\n' \
"//codex-rs/..." \
"-//codex-rs/core/tests/remote_env_windows:smoke-test" \
"-//codex-rs/v8-poc:all"
@@ -18,5 +18,8 @@ if [[ "${RUNNER_OS:-}" != "Windows" ]]; then
manual_rust_test_targets="$(printf '%s\n' "${manual_rust_test_targets}" | grep -v -- '-windows-cross-bin$' || true)"
fi
printf '%s\n' "//codex-rs/..."
# The lint configuration does not register the transitioned Windows toolchain.
printf '%s\n' \
"//codex-rs/..." \
"-//codex-rs/core/tests/remote_env_windows:smoke-test"
printf '%s\n' "${manual_rust_test_targets}"