mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
test: run app-server integration tests with Wine exec
This commit is contained in:
Generated
+4
@@ -402,16 +402,20 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-models-manager",
|
||||
"codex-protocol",
|
||||
"codex-utils-cargo-bin",
|
||||
"core_test_support",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"uuid",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@ codex_rust_crate(
|
||||
"//codex-rs/rmcp-client:test_stdio_server",
|
||||
],
|
||||
integration_test_timeout = "long",
|
||||
run_tests_with_wine_exec = True,
|
||||
test_shard_counts = {
|
||||
# Note app-server-all-test has a large number of integration tests, so
|
||||
# even a single shard can be quite slow. When there is a legitimate
|
||||
|
||||
@@ -4,4 +4,5 @@ codex_rust_crate(
|
||||
name = "common",
|
||||
crate_name = "app_test_support",
|
||||
crate_srcs = glob(["*.rs"]),
|
||||
run_tests_with_wine_exec = True,
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ chrono = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
@@ -32,7 +33,12 @@ tokio = { workspace = true, features = [
|
||||
"process",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
toml = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
wiremock = { workspace = true }
|
||||
core_test_support = { path = "../../../core/tests/common" }
|
||||
shlex = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -46,7 +46,9 @@ pub use rollout::rollout_path;
|
||||
use serde::de::DeserializeOwned;
|
||||
pub use test_app_server::DEFAULT_CLIENT_NAME;
|
||||
pub use test_app_server::DISABLE_PLUGIN_STARTUP_TASKS_ARG;
|
||||
pub use test_app_server::EnvironmentConfigFile;
|
||||
pub use test_app_server::TestAppServer;
|
||||
pub use test_app_server::TestAppServerEnvironment;
|
||||
|
||||
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
|
||||
let value = serde_json::to_value(response.result)?;
|
||||
|
||||
@@ -107,13 +107,153 @@ use codex_app_server_protocol::ThreadTurnsListParams;
|
||||
use codex_app_server_protocol::ThreadUnarchiveParams;
|
||||
use codex_app_server_protocol::ThreadUnsubscribeParams;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnEnvironmentParams;
|
||||
use codex_app_server_protocol::TurnInterruptParams;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnSteerParams;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupStartParams;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_login::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR;
|
||||
use core_test_support::TestEnvironment;
|
||||
use core_test_support::TestExecutionEnvironment;
|
||||
use core_test_support::test_environment;
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum LocalEnvironmentRegistration {
|
||||
Included,
|
||||
Omitted,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct GeneratedEnvironmentsToml<'a> {
|
||||
default: &'a str,
|
||||
include_local: bool,
|
||||
environments: [GeneratedEnvironmentToml<'a>; 1],
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct GeneratedEnvironmentToml<'a> {
|
||||
id: &'a str,
|
||||
url: &'a str,
|
||||
}
|
||||
|
||||
const GENERATED_ENVIRONMENTS_TOML_HEADER: &str =
|
||||
"# Generated by app_test_support for remote execution.\n";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum EnvironmentConfigFile {
|
||||
Missing,
|
||||
HarnessGenerated,
|
||||
TestOwned,
|
||||
}
|
||||
|
||||
/// Adapts the shared integration-test execution environment for app-server tests.
|
||||
pub struct TestAppServerEnvironment {
|
||||
state: TestAppServerEnvironmentState,
|
||||
}
|
||||
|
||||
enum TestAppServerEnvironmentState {
|
||||
Local,
|
||||
Remote {
|
||||
execution: Box<TestExecutionEnvironment>,
|
||||
generated_thread_environment: Option<TurnEnvironmentParams>,
|
||||
local_environment_registration: LocalEnvironmentRegistration,
|
||||
},
|
||||
}
|
||||
|
||||
impl TestAppServerEnvironment {
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
let local_environment_registration = match test_environment() {
|
||||
TestEnvironment::Local => {
|
||||
return Ok(Self {
|
||||
state: TestAppServerEnvironmentState::Local,
|
||||
});
|
||||
}
|
||||
TestEnvironment::Docker { .. } => LocalEnvironmentRegistration::Included,
|
||||
TestEnvironment::WineExec => LocalEnvironmentRegistration::Omitted,
|
||||
};
|
||||
Ok(Self {
|
||||
state: TestAppServerEnvironmentState::Remote {
|
||||
execution: Box::new(TestExecutionEnvironment::new().await?),
|
||||
generated_thread_environment: None,
|
||||
local_environment_registration,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generated_environments_toml(
|
||||
&mut self,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
environment_config_file: EnvironmentConfigFile,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
let TestAppServerEnvironmentState::Remote {
|
||||
execution,
|
||||
generated_thread_environment,
|
||||
local_environment_registration,
|
||||
} = &mut self.state
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
// A caller-provided `CODEX_EXEC_SERVER_URL` override, including removal, is intentional.
|
||||
// Do not generate a file because `environments.toml` takes precedence and would mask it.
|
||||
let exec_server_url_overridden = env_overrides
|
||||
.iter()
|
||||
.any(|(key, _value)| *key == CODEX_EXEC_SERVER_URL_ENV_VAR);
|
||||
if exec_server_url_overridden || environment_config_file == EnvironmentConfigFile::TestOwned
|
||||
{
|
||||
if matches!(test_environment(), TestEnvironment::WineExec) {
|
||||
panic!(
|
||||
"Wine-exec app-server tests cannot use a caller-provided \
|
||||
`CODEX_EXEC_SERVER_URL` or `environments.toml` because the harness cannot \
|
||||
guarantee that the app server selects its Wine executor with a \
|
||||
Windows-native cwd. Add a `skip_if_wine_exec!` guard with a TODO describing \
|
||||
how to support this configuration across operating systems."
|
||||
);
|
||||
}
|
||||
*generated_thread_environment = None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let exec_server_url = execution
|
||||
.exec_server_url()
|
||||
.context("remote test environment should have an exec-server URL")?;
|
||||
let environment_id = execution.environment_id();
|
||||
let include_local = match local_environment_registration {
|
||||
LocalEnvironmentRegistration::Included => true,
|
||||
LocalEnvironmentRegistration::Omitted => false,
|
||||
};
|
||||
*generated_thread_environment = Some(TurnEnvironmentParams {
|
||||
environment_id: environment_id.to_string(),
|
||||
cwd: execution.environment_cwd().clone(),
|
||||
});
|
||||
if environment_config_file == EnvironmentConfigFile::HarnessGenerated {
|
||||
return Ok(None);
|
||||
}
|
||||
let contents = toml::to_string(&GeneratedEnvironmentsToml {
|
||||
default: environment_id,
|
||||
include_local,
|
||||
environments: [GeneratedEnvironmentToml {
|
||||
id: environment_id,
|
||||
url: exec_server_url,
|
||||
}],
|
||||
})?;
|
||||
Ok(Some(format!(
|
||||
"{GENERATED_ENVIRONMENTS_TOML_HEADER}{contents}"
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn generated_thread_environment(&self) -> Option<TurnEnvironmentParams> {
|
||||
match &self.state {
|
||||
TestAppServerEnvironmentState::Local => None,
|
||||
TestAppServerEnvironmentState::Remote {
|
||||
generated_thread_environment,
|
||||
..
|
||||
} => generated_thread_environment.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestAppServer {
|
||||
next_request_id: AtomicI64,
|
||||
/// Retain this child process until the client is dropped. The Tokio runtime
|
||||
@@ -124,6 +264,12 @@ pub struct TestAppServer {
|
||||
stdin: Option<ChildStdin>,
|
||||
stdout: BufReader<ChildStdout>,
|
||||
pending_messages: VecDeque<JSONRPCMessage>,
|
||||
/// Retains the environment resolved before app-server starts because it owns the remote
|
||||
/// workspace and records whether startup configuration was harness-generated. Recomputing it
|
||||
/// for `thread/start` would allocate a different cwd and lose that registration decision.
|
||||
test_environment: TestAppServerEnvironment,
|
||||
/// Remembers the initialize capability because later `thread/start` requests do not carry it.
|
||||
experimental_api_enabled: bool,
|
||||
}
|
||||
|
||||
pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
|
||||
@@ -216,6 +362,7 @@ impl TestAppServer {
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
args: &[&str],
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut test_environment = TestAppServerEnvironment::new().await?;
|
||||
let mut cmd = Command::new(program);
|
||||
|
||||
cmd.stdin(Stdio::piped());
|
||||
@@ -232,6 +379,41 @@ impl TestAppServer {
|
||||
cmd.env_remove(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR);
|
||||
cmd.args(args);
|
||||
|
||||
// Register the selected remote test executor at process startup, independently of the
|
||||
// client's experimental API capability. Wine-exec intentionally omits the local
|
||||
// environment so compatible operations cannot accidentally exercise the host executor.
|
||||
// Reuse harness-generated configuration across app-server restarts. Explicit overrides
|
||||
// and test-owned configuration remain untouched; Wine rejects those escape hatches so a
|
||||
// test cannot silently fall back to an executor with the wrong path convention.
|
||||
let environments_toml = codex_home.join("environments.toml");
|
||||
let environment_config_file = match std::fs::read_to_string(&environments_toml) {
|
||||
Ok(contents) if contents.starts_with(GENERATED_ENVIRONMENTS_TOML_HEADER) => {
|
||||
EnvironmentConfigFile::HarnessGenerated
|
||||
}
|
||||
Ok(_contents) => EnvironmentConfigFile::TestOwned,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
EnvironmentConfigFile::Missing
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(error).with_context(|| {
|
||||
format!(
|
||||
"read environment config from {}",
|
||||
environments_toml.display()
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
if let Some(contents) =
|
||||
test_environment.generated_environments_toml(env_overrides, environment_config_file)?
|
||||
{
|
||||
std::fs::write(&environments_toml, contents).with_context(|| {
|
||||
format!(
|
||||
"write test environment config to {}",
|
||||
environments_toml.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
for (k, v) in env_overrides {
|
||||
match v {
|
||||
Some(val) => {
|
||||
@@ -273,6 +455,8 @@ impl TestAppServer {
|
||||
stdin: Some(stdin),
|
||||
stdout,
|
||||
pending_messages: VecDeque::new(),
|
||||
test_environment,
|
||||
experimental_api_enabled: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -322,8 +506,8 @@ impl TestAppServer {
|
||||
&mut self,
|
||||
params: InitializeParams,
|
||||
) -> anyhow::Result<JSONRPCMessage> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
let request_id = self.send_request("initialize", params).await?;
|
||||
let request_params = Some(serde_json::to_value(¶ms)?);
|
||||
let request_id = self.send_request("initialize", request_params).await?;
|
||||
let message = self.read_jsonrpc_message().await?;
|
||||
match message {
|
||||
JSONRPCMessage::Response(response) => {
|
||||
@@ -339,6 +523,14 @@ impl TestAppServer {
|
||||
self.send_notification(ClientNotification::Initialized)
|
||||
.await?;
|
||||
|
||||
// Client capabilities are negotiated during `initialize`; `thread/start` does
|
||||
// not repeat them. Retain this bit after a successful handshake so the harness
|
||||
// knows whether it may inject the experimental `environments` field later.
|
||||
self.experimental_api_enabled = params
|
||||
.capabilities
|
||||
.as_ref()
|
||||
.is_some_and(|capabilities| capabilities.experimental_api);
|
||||
|
||||
Ok(JSONRPCMessage::Response(response))
|
||||
}
|
||||
JSONRPCMessage::Error(error) => {
|
||||
@@ -443,8 +635,30 @@ impl TestAppServer {
|
||||
/// Send a `thread/start` JSON-RPC request.
|
||||
pub async fn send_thread_start_request(
|
||||
&mut self,
|
||||
params: ThreadStartParams,
|
||||
mut params: ThreadStartParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
// `environments.toml` registers and defaults the remote executor, but websocket
|
||||
// environments cannot configure a cwd. Core would therefore pair that executor with the
|
||||
// Linux app-server's `Config::cwd`. When the client negotiated experimental API support,
|
||||
// the harness automatically injects the generated environment with its target-native
|
||||
// Wine/Docker cwd on `thread/start`.
|
||||
let generated_environment =
|
||||
if self.experimental_api_enabled && params.environments.is_none() {
|
||||
self.test_environment.generated_thread_environment()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(environment) = generated_environment {
|
||||
params.environments = Some(vec![environment]);
|
||||
} else if matches!(test_environment(), TestEnvironment::WineExec) {
|
||||
panic!(
|
||||
"Wine-exec app-server tests must start threads with the harness-generated remote \
|
||||
environment so the executor receives a Windows-native cwd. This test uses a \
|
||||
non-experimental client, explicit thread environments, or test-owned environment \
|
||||
configuration, so the harness cannot safely inject that selection. Add a \
|
||||
`skip_if_wine_exec!` guard with a TODO describing how to make this setup cross-OS."
|
||||
);
|
||||
}
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("thread/start", params).await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use app_test_support::EnvironmentConfigFile;
|
||||
use app_test_support::TestAppServerEnvironment;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
|
||||
use core_test_support::TestEnvironment;
|
||||
use core_test_support::test_environment;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn generated_config_registers_selected_remote_as_default() -> Result<()> {
|
||||
let mut environment = TestAppServerEnvironment::new().await?;
|
||||
let environments_toml =
|
||||
environment.generated_environments_toml(&[], EnvironmentConfigFile::Missing)?;
|
||||
|
||||
match test_environment() {
|
||||
TestEnvironment::Local => assert_eq!(environments_toml, None),
|
||||
TestEnvironment::Docker { .. } | TestEnvironment::WineExec => {
|
||||
let contents = environments_toml.context("generated environments.toml")?;
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(codex_home.path().join("environments.toml"), &contents)?;
|
||||
let local_runtime_paths = ExecServerRuntimePaths::new(
|
||||
std::env::current_exe()?,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)?;
|
||||
let manager =
|
||||
EnvironmentManager::from_codex_home(codex_home.path(), Some(local_runtime_paths))
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
manager.default_environment_id(),
|
||||
Some(REMOTE_ENVIRONMENT_ID)
|
||||
);
|
||||
match test_environment() {
|
||||
TestEnvironment::Local => unreachable!("remote test branch"),
|
||||
TestEnvironment::Docker { .. } => {
|
||||
assert!(manager.try_local_environment().is_some());
|
||||
}
|
||||
TestEnvironment::WineExec => {
|
||||
assert!(manager.try_local_environment().is_none());
|
||||
}
|
||||
}
|
||||
let remote = manager
|
||||
.get_environment(REMOTE_ENVIRONMENT_ID)
|
||||
.context("remote environment")?;
|
||||
assert!(remote.exec_server_url().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generated_config_respects_explicit_overrides_or_rejects_them_under_wine() -> Result<()> {
|
||||
let mut environment = TestAppServerEnvironment::new().await?;
|
||||
let generated = environment.generated_environments_toml(&[], EnvironmentConfigFile::Missing)?;
|
||||
|
||||
match test_environment() {
|
||||
TestEnvironment::Local => {
|
||||
assert_eq!(generated, None);
|
||||
assert_eq!(
|
||||
environment.generated_environments_toml(
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
|
||||
EnvironmentConfigFile::Missing,
|
||||
)?,
|
||||
None
|
||||
);
|
||||
}
|
||||
TestEnvironment::Docker { .. } => {
|
||||
assert!(generated.is_some());
|
||||
|
||||
for override_value in [Some("none"), None] {
|
||||
assert_eq!(
|
||||
environment.generated_environments_toml(
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, override_value)],
|
||||
EnvironmentConfigFile::Missing,
|
||||
)?,
|
||||
None
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
environment.generated_environments_toml(&[], EnvironmentConfigFile::TestOwned)?,
|
||||
None
|
||||
);
|
||||
}
|
||||
TestEnvironment::WineExec => {
|
||||
assert!(generated.is_some());
|
||||
|
||||
for override_value in [Some("none"), None] {
|
||||
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
environment.generated_environments_toml(
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, override_value)],
|
||||
EnvironmentConfigFile::Missing,
|
||||
)
|
||||
}));
|
||||
assert!(panic.is_err());
|
||||
}
|
||||
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
environment.generated_environments_toml(&[], EnvironmentConfigFile::TestOwned)
|
||||
}));
|
||||
assert!(panic.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_environment_is_available_only_for_harness_generated_config() -> Result<()> {
|
||||
match test_environment() {
|
||||
TestEnvironment::Local => {
|
||||
let mut environment = TestAppServerEnvironment::new().await?;
|
||||
assert_eq!(
|
||||
environment.generated_environments_toml(&[], EnvironmentConfigFile::Missing)?,
|
||||
None
|
||||
);
|
||||
assert_eq!(environment.generated_thread_environment(), None);
|
||||
}
|
||||
TestEnvironment::Docker { .. } | TestEnvironment::WineExec => {
|
||||
let mut generated_environment = TestAppServerEnvironment::new().await?;
|
||||
generated_environment
|
||||
.generated_environments_toml(&[], EnvironmentConfigFile::Missing)?
|
||||
.context("generated environments.toml")?;
|
||||
let selected = generated_environment
|
||||
.generated_thread_environment()
|
||||
.context("generated thread environment")?;
|
||||
assert_eq!(selected.environment_id, REMOTE_ENVIRONMENT_ID);
|
||||
assert!(selected.cwd.to_inferred_path_uri().is_some());
|
||||
|
||||
let mut restarted_environment = TestAppServerEnvironment::new().await?;
|
||||
assert_eq!(
|
||||
restarted_environment
|
||||
.generated_environments_toml(&[], EnvironmentConfigFile::HarnessGenerated,)?,
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
restarted_environment
|
||||
.generated_thread_environment()
|
||||
.is_some()
|
||||
);
|
||||
|
||||
match test_environment() {
|
||||
TestEnvironment::Local => unreachable!("remote test branch"),
|
||||
TestEnvironment::Docker { .. } => {
|
||||
let mut configured_environment = TestAppServerEnvironment::new().await?;
|
||||
assert_eq!(
|
||||
configured_environment
|
||||
.generated_environments_toml(&[], EnvironmentConfigFile::TestOwned,)?,
|
||||
None
|
||||
);
|
||||
assert_eq!(configured_environment.generated_thread_environment(), None);
|
||||
|
||||
let mut overridden_environment = TestAppServerEnvironment::new().await?;
|
||||
assert_eq!(
|
||||
overridden_environment.generated_environments_toml(
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("test-owned"))],
|
||||
EnvironmentConfigFile::Missing,
|
||||
)?,
|
||||
None
|
||||
);
|
||||
assert_eq!(overridden_environment.generated_thread_environment(), None);
|
||||
}
|
||||
TestEnvironment::WineExec => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -19,6 +19,7 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SandboxPolicy;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
@@ -39,6 +40,9 @@ use super::connection_handling_websocket::spawn_websocket_server;
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_without_streams_can_be_terminated() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -89,6 +93,9 @@ async fn command_exec_without_streams_can_be_terminated() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_without_process_id_keeps_buffered_compatibility() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -137,6 +144,9 @@ async fn command_exec_without_process_id_keeps_buffered_compatibility() -> Resul
|
||||
#[tokio::test]
|
||||
async fn command_exec_env_overrides_merge_with_server_environment_and_support_unset() -> Result<()>
|
||||
{
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -195,6 +205,9 @@ async fn command_exec_env_overrides_merge_with_server_environment_and_support_un
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_accepts_permission_profile() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -242,6 +255,9 @@ async fn command_exec_accepts_permission_profile() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_permission_profile_starts_selected_network_proxy() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -293,6 +309,9 @@ async fn command_exec_permission_profile_starts_selected_network_proxy() -> Resu
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_permission_profile_does_not_reuse_default_network_proxy() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -342,6 +361,9 @@ async fn command_exec_permission_profile_does_not_reuse_default_network_proxy()
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
let command_dir = codex_home.path().join("command-cwd");
|
||||
@@ -403,6 +425,10 @@ async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Resu
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_returns_error_when_local_environment_is_disabled() -> Result<()> {
|
||||
// TODO(anp): Cover the command/exec disabled-environment error with a cross-OS test
|
||||
// configuration that does not replace the Wine harness's executor registration.
|
||||
skip_if_wine_exec!(Ok(()), "uses an explicit executor-disable override");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -442,6 +468,9 @@ async fn command_exec_returns_error_when_local_environment_is_disabled() -> Resu
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_rejects_sandbox_policy_with_permission_profile() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -480,6 +509,9 @@ async fn command_exec_rejects_sandbox_policy_with_permission_profile() -> Result
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_rejects_disable_timeout_with_timeout_ms() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -518,6 +550,9 @@ async fn command_exec_rejects_disable_timeout_with_timeout_ms() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_rejects_disable_output_cap_with_output_bytes_cap() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -556,6 +591,9 @@ async fn command_exec_rejects_disable_output_cap_with_output_bytes_cap() -> Resu
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_rejects_negative_timeout_ms() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -594,6 +632,9 @@ async fn command_exec_rejects_negative_timeout_ms() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_without_process_id_rejects_streaming() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -632,6 +673,9 @@ async fn command_exec_without_process_id_rejects_streaming() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_non_streaming_respects_output_cap() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -679,6 +723,9 @@ async fn command_exec_non_streaming_respects_output_cap() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_streaming_does_not_buffer_output() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -743,6 +790,9 @@ async fn command_exec_streaming_does_not_buffer_output() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_pipe_streams_output_and_accepts_write() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -819,6 +869,9 @@ async fn command_exec_pipe_streams_output_and_accepts_write() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_tty_implies_streaming_and_reports_pty_output() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -890,6 +943,9 @@ async fn command_exec_tty_implies_streaming_and_reports_pty_output() -> Result<(
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_tty_supports_initial_size_and_resize() -> Result<()> {
|
||||
// TODO(anp): Let command/exec target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "command/exec requires the local environment");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use std::process::Command as StdCommand;
|
||||
@@ -33,6 +34,9 @@ use wiremock::matchers::path_regex;
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_ctrl_c_waits_for_running_turn_before_exit() -> Result<()> {
|
||||
// TODO(anp): Add Windows console-control coverage equivalent to this Unix signal behavior.
|
||||
skip_if_wine_exec!(Ok(()), "tests Unix signal shutdown");
|
||||
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
@@ -58,6 +62,9 @@ async fn websocket_transport_ctrl_c_waits_for_running_turn_before_exit() -> Resu
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_second_ctrl_c_forces_exit_while_turn_running() -> Result<()> {
|
||||
// TODO(anp): Add Windows console-control coverage equivalent to this Unix signal behavior.
|
||||
skip_if_wine_exec!(Ok(()), "tests Unix signal shutdown");
|
||||
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
@@ -84,6 +91,9 @@ async fn websocket_transport_second_ctrl_c_forces_exit_while_turn_running() -> R
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_sigterm_waits_for_running_turn_before_exit() -> Result<()> {
|
||||
// TODO(anp): Add Windows console-control coverage equivalent to this Unix signal behavior.
|
||||
skip_if_wine_exec!(Ok(()), "tests Unix signal shutdown");
|
||||
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
@@ -109,6 +119,9 @@ async fn websocket_transport_sigterm_waits_for_running_turn_before_exit() -> Res
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_second_sigterm_forces_exit_while_turn_running() -> Result<()> {
|
||||
// TODO(anp): Add Windows console-control coverage equivalent to this Unix signal behavior.
|
||||
skip_if_wine_exec!(Ok(()), "tests Unix signal shutdown");
|
||||
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
@@ -135,6 +148,9 @@ async fn websocket_transport_second_sigterm_forces_exit_while_turn_running() ->
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_repeated_sighup_keeps_waiting_for_running_turn() -> Result<()> {
|
||||
// TODO(anp): Add Windows console-control coverage equivalent to this Unix signal behavior.
|
||||
skip_if_wine_exec!(Ok(()), "tests Unix signal shutdown");
|
||||
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
|
||||
@@ -6,6 +6,7 @@ use app_test_support::to_response;
|
||||
use codex_app_server_protocol::EnvironmentAddResponse;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::AsyncReadExt;
|
||||
@@ -17,6 +18,10 @@ const CONNECTION_CLOSE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_add_applies_connect_timeout() -> Result<()> {
|
||||
// TODO(anp): Cover environment/add timeouts in cross-OS configurations by registering a
|
||||
// second target-native environment without relying on the injected default selection.
|
||||
skip_if_wine_exec!(Ok(()), "uses an explicit environment");
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let exec_server_url = format!("ws://{}", listener.local_addr()?);
|
||||
let stalled_server = tokio::spawn(async move {
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use core_test_support::stdio_server_bin;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
@@ -32,6 +33,10 @@ const TOOL_CALL_ID: &str = "executor-mcp-call";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn selected_executor_plugin_exposes_its_stdio_mcp_only_to_that_thread() -> Result<()> {
|
||||
// TODO(anp): Support executor-provided stdio MCP servers across host/target OS combinations by
|
||||
// using the host runfile locally and a copied target-native path remotely.
|
||||
skip_if_wine_exec!(Ok(()), "uses a host-local stdio MCP fixture");
|
||||
|
||||
let responses_server = responses::start_mock_server().await;
|
||||
let codex_home = TempDir::new()?;
|
||||
write_mock_responses_config_toml(
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -22,6 +23,10 @@ const LOCAL_SKILL_MARKER: &str = "LOCAL_SKILL_BODY_MARKER";
|
||||
|
||||
#[tokio::test]
|
||||
async fn selected_executor_root_exposes_plugin_skill() -> Result<()> {
|
||||
// TODO(anp): Support executor plugin skills across host/target OS combinations by staging the
|
||||
// fixture in the selected environment and using its target-native capability-root path.
|
||||
skip_if_wine_exec!(Ok(()), "uses an explicit host-local capability root");
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_app_server_protocol::ThreadSettingsUpdateParams;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_protocol::protocol::RealtimeOutputModality;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
@@ -225,6 +226,10 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_mock_field_requires_experimental_api_capability() -> Result<()> {
|
||||
// TODO(anp): Let the Wine harness supply a target-native default cwd without using the
|
||||
// experimental thread environment field so this capability check can run cross-OS.
|
||||
skip_if_wine_exec!(Ok(()), "requires a non-experimental thread/start");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
@@ -264,6 +269,10 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul
|
||||
#[tokio::test]
|
||||
async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capability()
|
||||
-> Result<()> {
|
||||
// TODO(anp): Let the Wine harness supply a target-native default cwd without using the
|
||||
// experimental thread environment field so this capability check can run cross-OS.
|
||||
skip_if_wine_exec!(Ok(()), "requires a non-experimental thread/start");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
@@ -302,6 +311,10 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa
|
||||
#[tokio::test]
|
||||
async fn thread_start_granular_approval_policy_requires_experimental_api_capability() -> Result<()>
|
||||
{
|
||||
// TODO(anp): Let the Wine harness supply a target-native default cwd without using the
|
||||
// experimental thread environment field so this capability check can run cross-OS.
|
||||
skip_if_wine_exec!(Ok(()), "requires a non-experimental thread/start");
|
||||
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
@@ -16,6 +16,7 @@ use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
@@ -67,6 +68,9 @@ fn absolute_path(path: PathBuf) -> AbsolutePathBuf {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
|
||||
// TODO(anp): Let fs RPCs target the selected filesystem in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let file_path = codex_home.path().join("note.txt");
|
||||
std::fs::write(&file_path, "hello")?;
|
||||
@@ -121,6 +125,10 @@ async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_methods_return_error_when_local_environment_is_disabled() -> Result<()> {
|
||||
// TODO(anp): Cover the fs RPC disabled-environment errors with a cross-OS test configuration
|
||||
// that does not replace the Wine harness's executor registration.
|
||||
skip_if_wine_exec!(Ok(()), "uses an explicit executor-disable override");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let absolute_file = codex_home.path().join("absolute.txt");
|
||||
|
||||
@@ -144,6 +152,9 @@ async fn fs_methods_return_error_when_local_environment_is_disabled() -> Result<
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_get_metadata_reports_symlink() -> Result<()> {
|
||||
// TODO(anp): Add Windows reparse-point coverage equivalent to this Unix symlink metadata case.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let file_path = codex_home.path().join("note.txt");
|
||||
let symlink_path = codex_home.path().join("note-link.txt");
|
||||
@@ -172,6 +183,9 @@ async fn fs_get_metadata_reports_symlink() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
|
||||
// TODO(anp): Let fs RPCs target the selected filesystem in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let source_dir = codex_home.path().join("source");
|
||||
let nested_dir = source_dir.join("nested");
|
||||
@@ -323,6 +337,9 @@ async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_write_file_accepts_base64_bytes() -> Result<()> {
|
||||
// TODO(anp): Let fs RPCs target the selected filesystem in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let file_path = codex_home.path().join("blob.bin");
|
||||
let bytes = [0_u8, 1, 2, 255];
|
||||
@@ -518,6 +535,9 @@ async fn fs_methods_reject_relative_paths() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_copy_rejects_directory_without_recursive() -> Result<()> {
|
||||
// TODO(anp): Let fs RPCs target the selected filesystem in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let source_dir = codex_home.path().join("source");
|
||||
std::fs::create_dir_all(&source_dir)?;
|
||||
@@ -545,6 +565,9 @@ async fn fs_copy_rejects_directory_without_recursive() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_copy_rejects_copying_directory_into_descendant() -> Result<()> {
|
||||
// TODO(anp): Let fs RPCs target the selected filesystem in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let source_dir = codex_home.path().join("source");
|
||||
std::fs::create_dir_all(source_dir.join("nested"))?;
|
||||
@@ -573,6 +596,9 @@ async fn fs_copy_rejects_copying_directory_into_descendant() -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_copy_preserves_symlinks_in_recursive_copy() -> Result<()> {
|
||||
// TODO(anp): Add Windows reparse-point coverage equivalent to this Unix symlink-copy case.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let source_dir = codex_home.path().join("source");
|
||||
let nested_dir = source_dir.join("nested");
|
||||
@@ -605,6 +631,10 @@ async fn fs_copy_preserves_symlinks_in_recursive_copy() -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_copy_ignores_unknown_special_files_in_recursive_copy() -> Result<()> {
|
||||
// TODO(anp): Exercise this Unix FIFO-copy behavior through an explicit Unix environment when
|
||||
// the default environment uses Windows paths.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let source_dir = codex_home.path().join("source");
|
||||
let copied_dir = codex_home.path().join("copied");
|
||||
@@ -646,6 +676,10 @@ async fn fs_copy_ignores_unknown_special_files_in_recursive_copy() -> Result<()>
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_copy_rejects_standalone_fifo_source() -> Result<()> {
|
||||
// TODO(anp): Exercise this Unix FIFO rejection through an explicit Unix environment when the
|
||||
// default environment uses Windows paths.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let fifo_path = codex_home.path().join("named-pipe");
|
||||
let output = Command::new("mkfifo").arg(&fifo_path).output()?;
|
||||
@@ -678,6 +712,9 @@ async fn fs_copy_rejects_standalone_fifo_source() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifications()
|
||||
-> Result<()> {
|
||||
// TODO(anp): Let fs RPCs target the selected filesystem in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let git_dir = codex_home.path().join("repo").join(".git");
|
||||
let fetch_head = git_dir.join("FETCH_HEAD");
|
||||
@@ -746,6 +783,9 @@ async fn fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifi
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_watch_file_reports_atomic_replace_events() -> Result<()> {
|
||||
// TODO(anp): Let fs RPCs target the selected filesystem in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let git_dir = codex_home.path().join("repo").join(".git");
|
||||
let head_path = git_dir.join("HEAD");
|
||||
@@ -786,6 +826,9 @@ async fn fs_watch_file_reports_atomic_replace_events() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_watch_allows_missing_file_targets() -> Result<()> {
|
||||
// TODO(anp): Let fs RPCs target the selected filesystem in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "fs RPCs require the local filesystem");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let git_dir = codex_home.path().join("repo").join(".git");
|
||||
let fetch_head = git_dir.join("FETCH_HEAD");
|
||||
|
||||
@@ -26,6 +26,7 @@ use codex_core::config::set_project_trust_level;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::skip_if_windows;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde::Serialize;
|
||||
use tempfile::TempDir;
|
||||
@@ -652,6 +653,9 @@ async fn config_batch_write_toggles_user_hook() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn config_batch_write_updates_hook_trust_for_loaded_session() -> Result<()> {
|
||||
skip_if_windows!(Ok(()));
|
||||
// TODO(anp): Support hook execution across host/target OS configurations by copying a
|
||||
// cross-platform fixture into the selected environment and reading its log there.
|
||||
skip_if_wine_exec!(Ok(()), "uses a host-local hook command");
|
||||
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("Warmup")?,
|
||||
@@ -903,6 +907,9 @@ command = "python3 {hook_script_path}"
|
||||
#[tokio::test]
|
||||
async fn config_batch_write_disables_hook_for_loaded_session() -> Result<()> {
|
||||
skip_if_windows!(Ok(()));
|
||||
// TODO(anp): Support hook execution across host/target OS configurations by copying a
|
||||
// cross-platform fixture into the selected environment and reading its log there.
|
||||
skip_if_wine_exec!(Ok(()), "uses a host-local hook command");
|
||||
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("Warmup")?,
|
||||
|
||||
@@ -18,6 +18,7 @@ use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
@@ -227,6 +228,10 @@ async fn standalone_image_generation_failure_emits_terminal_item() -> Result<()>
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_image_edit_uses_attached_model_visible_image() -> Result<()> {
|
||||
// TODO(anp): Support attached-image editing across host/target OS configurations by copying
|
||||
// the fixture into the selected environment and passing its target-native path.
|
||||
skip_if_wine_exec!(Ok(()), "uses a host-local image path");
|
||||
|
||||
let edit_request = run_image_edit_test(|codex_home| {
|
||||
let image_path = codex_home.join("attached.png");
|
||||
std::fs::write(&image_path, TINY_PNG_BYTES)?;
|
||||
|
||||
@@ -32,6 +32,7 @@ use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::handler::server::ServerHandler;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
@@ -129,6 +130,10 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn orchestrator_skill_can_read_referenced_resource_without_an_executor() -> Result<()> {
|
||||
// TODO(anp): Support orchestrator resource reads with an explicit empty environment list in
|
||||
// cross-OS configurations without injecting the remote default.
|
||||
skip_if_wine_exec!(Ok(()), "uses explicit environments");
|
||||
|
||||
let responses_server = responses::start_mock_server().await;
|
||||
let (apps_server_url, apps_server_calls, apps_server_handle) =
|
||||
start_resource_apps_mcp_server().await?;
|
||||
@@ -365,6 +370,10 @@ async fn orchestrator_skill_can_read_referenced_resource_without_an_executor() -
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn local_executor_does_not_expose_orchestrator_skills() -> Result<()> {
|
||||
// TODO(anp): Support local-executor capability filtering assertions in cross-OS
|
||||
// configurations that also select a remote default environment.
|
||||
skip_if_wine_exec!(Ok(()), "asserts local executor capabilities");
|
||||
|
||||
let responses_server = responses::start_mock_server().await;
|
||||
let (apps_server_url, _apps_server_calls, apps_server_handle) =
|
||||
start_resource_apps_mcp_server().await?;
|
||||
@@ -438,6 +447,10 @@ async fn local_executor_does_not_expose_orchestrator_skills() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn disabled_orchestrator_skills_do_not_expose_skills_namespace() -> Result<()> {
|
||||
// TODO(anp): Cover disabled orchestrator skills in cross-OS configurations without replacing
|
||||
// the harness-selected remote environment with an explicit empty selection.
|
||||
skip_if_wine_exec!(Ok(()), "explicitly disables thread environments");
|
||||
|
||||
let responses_server = responses::start_mock_server().await;
|
||||
let (apps_server_url, apps_server_calls, apps_server_handle) =
|
||||
start_resource_apps_mcp_server().await?;
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_app_server_protocol::ProcessSpawnParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
@@ -22,6 +23,9 @@ use super::connection_handling_websocket::create_config_toml;
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_spawn_returns_before_exit_and_emits_exit_notification() -> Result<()> {
|
||||
// TODO(anp): Let process RPCs target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "process RPCs require the local environment");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let (_server, mut mcp) = initialized_mcp(codex_home.path()).await?;
|
||||
|
||||
@@ -105,6 +109,10 @@ async fn process_spawn_returns_before_exit_and_emits_exit_notification() -> Resu
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_spawn_returns_error_when_local_environment_is_disabled() -> Result<()> {
|
||||
// TODO(anp): Cover the process/spawn disabled-environment error with a cross-OS test
|
||||
// configuration that does not replace the Wine harness's executor registration.
|
||||
skip_if_wine_exec!(Ok(()), "uses an explicit executor-disable override");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
@@ -132,6 +140,9 @@ async fn process_spawn_returns_error_when_local_environment_is_disabled() -> Res
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_spawn_reports_buffered_output_cap_reached() -> Result<()> {
|
||||
// TODO(anp): Let process RPCs target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "process RPCs require the local environment");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let (_server, mut mcp) = initialized_mcp(codex_home.path()).await?;
|
||||
|
||||
@@ -181,6 +192,9 @@ async fn process_spawn_reports_buffered_output_cap_reached() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_kill_terminates_running_process() -> Result<()> {
|
||||
// TODO(anp): Let process RPCs target the selected environment in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "process RPCs require the local environment");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let (_server, mut mcp) = initialized_mcp(codex_home.path()).await?;
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ use core_test_support::responses::WebSocketTestServer;
|
||||
use core_test_support::responses::start_websocket_server;
|
||||
use core_test_support::responses::start_websocket_server_with_headers;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
@@ -2491,6 +2492,9 @@ async fn websocket_v2_background_agent_progress_is_sent_before_function_output()
|
||||
#[tokio::test]
|
||||
async fn websocket_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
// TODO(anp): Support delegated shell execution across host/target OS configurations by
|
||||
// choosing the command for the selected execution environment.
|
||||
skip_if_wine_exec!(Ok(()), "uses a POSIX shell command");
|
||||
|
||||
// Phase 1: keep the two mocked OpenAI conversations explicit. The realtime sideband only
|
||||
// calls the `background_agent` function; the shell command is requested by the delegated
|
||||
|
||||
@@ -16,12 +16,17 @@ use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn request_permissions_round_trip() -> Result<()> {
|
||||
// TODO(anp): Support request_permissions across host/target OS configurations by preserving
|
||||
// the selected environment's cwd and granted filesystem paths without host projection.
|
||||
skip_if_wine_exec!(Ok(()), "requires host-native permission paths");
|
||||
|
||||
let codex_home = tempfile::TempDir::new()?;
|
||||
let responses = vec![
|
||||
create_request_permissions_sse_response("call1")?,
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
@@ -403,6 +404,10 @@ async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disable
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_list_skips_cwd_roots_when_environment_disabled() -> Result<()> {
|
||||
// TODO(anp): Cover disabled-environment skill discovery with a cross-OS test configuration
|
||||
// that does not replace the Wine harness's executor registration.
|
||||
skip_if_wine_exec!(Ok(()), "uses an explicit executor-disable override");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
write_skill(&codex_home, "home-skill")?;
|
||||
@@ -518,6 +523,10 @@ async fn skills_list_preserves_requested_cwd_order() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_list_uses_cached_result_until_force_reload() -> Result<()> {
|
||||
// TODO(anp): Support skills/list cache invalidation across host/target OS configurations by
|
||||
// reading each cwd through its selected environment.
|
||||
skip_if_wine_exec!(Ok(()), "uses a host-local skills cwd");
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
|
||||
@@ -740,6 +749,10 @@ async fn skills_extra_roots_set_updates_process_runtime_roots() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<()> {
|
||||
// TODO(anp): Support skills-change notifications across host/target OS configurations by
|
||||
// watching each user-skill root through its owning environment.
|
||||
skip_if_wine_exec!(Ok(()), "uses host-local skill watcher paths");
|
||||
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
write_mock_responses_config_toml_with_chatgpt_base_url(
|
||||
|
||||
@@ -85,6 +85,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::fs::FileTimes;
|
||||
@@ -262,6 +263,10 @@ async fn thread_resume_with_empty_path_uses_running_thread_id() -> Result<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_resume_running_thread_uses_cached_instruction_sources() -> Result<()> {
|
||||
// TODO(anp): Support cached instruction sources across host/target OS configurations by
|
||||
// seeding AGENTS.md in the selected environment and asserting its target-native path.
|
||||
skip_if_wine_exec!(Ok(()), "uses host-local instruction source paths");
|
||||
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
@@ -35,6 +35,7 @@ use codex_core::shell::default_user_shell;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
@@ -46,6 +47,10 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
|
||||
#[tokio::test]
|
||||
async fn thread_shell_command_history_responses_exclude_persisted_command_executions() -> Result<()>
|
||||
{
|
||||
// TODO(anp): Support thread/shellCommand history in cross-OS configurations by targeting the
|
||||
// selected environment instead of requiring the app-server host.
|
||||
skip_if_wine_exec!(Ok(()), "exercises the local thread shell command");
|
||||
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
@@ -182,6 +187,10 @@ async fn thread_shell_command_history_responses_exclude_persisted_command_execut
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_shell_command_returns_error_when_local_environment_is_disabled() -> Result<()> {
|
||||
// TODO(anp): Let the Wine harness distinguish an intentionally environment-disabled thread
|
||||
// from an unsafe fallback so this error path can run in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "intentionally disables all environments");
|
||||
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
@@ -225,6 +234,9 @@ async fn thread_shell_command_returns_error_when_local_environment_is_disabled()
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_shell_command_uses_existing_active_turn() -> Result<()> {
|
||||
// TODO(anp): Support thread/shellCommand on selected environments in cross-OS configurations.
|
||||
skip_if_wine_exec!(Ok(()), "thread/shellCommand requires the local environment");
|
||||
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
|
||||
@@ -33,6 +33,7 @@ use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
@@ -281,6 +282,10 @@ async fn thread_start_excludes_profile_workspace_roots_from_runtime_workspace_ro
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_rejects_unknown_environment_as_invalid_request() -> Result<()> {
|
||||
// TODO(anp): Support unknown-environment validation in cross-OS configurations while
|
||||
// preserving the caller's explicit selection over the injected default.
|
||||
skip_if_wine_exec!(Ok(()), "uses explicit environments");
|
||||
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -317,6 +322,10 @@ async fn thread_start_rejects_unknown_environment_as_invalid_request() -> Result
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_rejects_relative_environment_cwd_as_invalid_request() -> Result<()> {
|
||||
// TODO(anp): Exercise malformed environment cwd validation in cross-OS configurations without
|
||||
// replacing the Wine harness's required target-native selection.
|
||||
skip_if_wine_exec!(Ok(()), "uses an explicit invalid environment cwd");
|
||||
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
@@ -351,6 +360,10 @@ async fn thread_start_rejects_relative_environment_cwd_as_invalid_request() -> R
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_response_includes_loaded_instruction_sources() -> Result<()> {
|
||||
// TODO(anp): Support instruction-source reporting across host/target OS configurations by
|
||||
// seeding project AGENTS.md in the selected environment and preserving both path conventions.
|
||||
skip_if_wine_exec!(Ok(()), "uses host-local instruction source paths");
|
||||
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
@@ -442,6 +455,10 @@ async fn thread_start_response_excludes_empty_project_instruction_source() -> Re
|
||||
#[tokio::test]
|
||||
async fn thread_start_without_selected_environment_includes_only_global_instruction_source()
|
||||
-> Result<()> {
|
||||
// TODO(anp): Support explicit empty environment selections in cross-OS configurations while
|
||||
// suppressing project instruction discovery from the remote default.
|
||||
skip_if_wine_exec!(Ok(()), "uses explicit environments");
|
||||
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
|
||||
@@ -22,6 +22,7 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -30,6 +31,10 @@ const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_interrupt_aborts_running_turn() -> Result<()> {
|
||||
// TODO(anp): Support turn interruption across host/target OS configurations by creating the
|
||||
// workdir in the selected environment and using its native sleep command.
|
||||
skip_if_wine_exec!(Ok(()), "uses a host-local workdir and sleep command");
|
||||
|
||||
// Use a portable sleep command to keep the turn running.
|
||||
#[cfg(target_os = "windows")]
|
||||
let shell_command = vec![
|
||||
|
||||
@@ -81,6 +81,7 @@ use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
@@ -2191,6 +2192,9 @@ async fn turn_start_forwards_custom_local_image_detail() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
// TODO(anp): Support approval-policy changes across host/target OS configurations by using an
|
||||
// executor-native command and asserting the selected environment.
|
||||
skip_if_wine_exec!(Ok(()), "uses a host-local Python shell command");
|
||||
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().to_path_buf();
|
||||
@@ -2817,6 +2821,10 @@ stream_max_retries = 0
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_resolves_sticky_thread_local_environment_and_turn_overrides() -> Result<()> {
|
||||
// TODO(anp): Provide target-native Wine executor fixtures for sticky and turn-level
|
||||
// environment selection so this resolution matrix can run cross-OS.
|
||||
skip_if_wine_exec!(Ok(()), "uses test-owned environment configuration");
|
||||
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
@@ -2965,6 +2973,9 @@ fn environment_params(ids: Option<&[&str]>, cwd: &Path) -> Option<Vec<TurnEnviro
|
||||
#[tokio::test]
|
||||
async fn turn_start_file_change_approval_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
// TODO(anp): Support file-change approval across host/target OS configurations by creating the
|
||||
// patch workspace and asserting the result through the selected environment's filesystem.
|
||||
skip_if_wine_exec!(Ok(()), "uses a host-local patch workspace");
|
||||
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
@@ -3951,6 +3962,9 @@ config_file = "./custom-role.toml"
|
||||
#[tokio::test]
|
||||
async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
// TODO(anp): Support session-scoped file-change approval across host/target OS configurations
|
||||
// by creating and inspecting the patch workspace through the selected environment.
|
||||
skip_if_wine_exec!(Ok(()), "uses a host-local patch workspace");
|
||||
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
|
||||
@@ -7,4 +7,5 @@ codex_rust_crate(
|
||||
deps_extra = [
|
||||
"@crates//:similar",
|
||||
],
|
||||
run_tests_with_wine_exec = True,
|
||||
)
|
||||
|
||||
@@ -37,6 +37,7 @@ pub mod tracing;
|
||||
pub mod zsh_fork;
|
||||
|
||||
pub use test_environment::TestEnvironment;
|
||||
pub use test_environment::TestExecutionEnvironment;
|
||||
pub use test_environment::get_remote_test_env;
|
||||
pub use test_environment::test_environment;
|
||||
|
||||
|
||||
@@ -3,16 +3,13 @@ use std::io::ErrorKind;
|
||||
use std::mem::swap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::StartThreadOptions;
|
||||
@@ -57,9 +54,6 @@ use serde_json::Value;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::MockServer;
|
||||
|
||||
use crate::TempDirExt;
|
||||
use crate::TestEnvironment;
|
||||
use crate::get_remote_test_env;
|
||||
use crate::load_default_config_for_test;
|
||||
use crate::load_default_config_for_test_with_cloud_config_bundle;
|
||||
use crate::responses::WebSocketTestServer;
|
||||
@@ -76,10 +70,11 @@ type PreBuildHook = dyn FnOnce(&Path) + Send + 'static;
|
||||
type WorkspaceSetup = dyn FnOnce(AbsolutePathBuf, Arc<dyn ExecutorFileSystem>) -> BoxFuture<'static, Result<()>>
|
||||
+ Send;
|
||||
const TEST_MODEL_WITH_EXPERIMENTAL_TOOLS: &str = "test-gpt-5.1-codex";
|
||||
const REMOTE_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_TEST_REMOTE_EXEC_SERVER_URL";
|
||||
static REMOTE_TEST_INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
const SUBMIT_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub use crate::test_environment::TestExecutionEnvironment as TestEnv;
|
||||
pub use crate::test_environment::test_execution_environment as test_env;
|
||||
|
||||
pub struct RecordingUserInstructionsProvider {
|
||||
inner: Arc<dyn UserInstructionsProvider>,
|
||||
load_count: AtomicUsize,
|
||||
@@ -116,130 +111,6 @@ pub fn local_selections(cwd: AbsolutePathBuf) -> TurnEnvironmentSelections {
|
||||
TurnEnvironmentSelections::new(cwd.clone(), vec![local(cwd)])
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestEnv {
|
||||
environment: codex_exec_server::Environment,
|
||||
exec_server_url: Option<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
local_cwd_temp_dir: Option<Arc<TempDir>>,
|
||||
remote_container_name: Option<String>,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
pub async fn local() -> Result<Self> {
|
||||
let local_cwd_temp_dir = Arc::new(TempDir::new()?);
|
||||
let cwd = local_cwd_temp_dir.abs();
|
||||
let environment =
|
||||
codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None)?;
|
||||
Ok(Self {
|
||||
environment,
|
||||
exec_server_url: None,
|
||||
cwd,
|
||||
local_cwd_temp_dir: Some(local_cwd_temp_dir),
|
||||
remote_container_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cwd(&self) -> &AbsolutePathBuf {
|
||||
&self.cwd
|
||||
}
|
||||
|
||||
pub fn environment(&self) -> &codex_exec_server::Environment {
|
||||
&self.environment
|
||||
}
|
||||
|
||||
fn local_cwd_temp_dir(&self) -> Option<Arc<TempDir>> {
|
||||
self.local_cwd_temp_dir.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestEnv {
|
||||
fn drop(&mut self) {
|
||||
if let Some(container_name) = &self.remote_container_name {
|
||||
let script = format!("rm -rf {}", self.cwd.as_path().display());
|
||||
let _ = docker_command_capture_stdout(["exec", container_name, "sh", "-lc", &script]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_env() -> Result<TestEnv> {
|
||||
match get_remote_test_env() {
|
||||
Some(remote_env) => {
|
||||
let websocket_url = remote_exec_server_url()?;
|
||||
let environment =
|
||||
codex_exec_server::Environment::create_for_tests(Some(websocket_url.clone()))?;
|
||||
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(
|
||||
&cwd_uri,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
let cwd = if remote_env == TestEnvironment::WineExec {
|
||||
// TODO(anp): Convert `Config::cwd` to `LegacyAppPathString` and remove this
|
||||
// compatibility projection.
|
||||
// `Config::cwd` still requires `AbsolutePathBuf`. Preserve the test harness's
|
||||
// Linux-absolute `/C:/...` compatibility spelling so converting it back to a
|
||||
// `PathUri` recovers the remote Windows convention. Production conversions stay
|
||||
// strict: `PathUri::to_abs_path` intentionally rejects foreign paths.
|
||||
let path = cwd_uri.to_url().to_file_path().map_err(|()| {
|
||||
anyhow!("remote test cwd URI cannot be projected onto the host: {cwd_uri}")
|
||||
})?;
|
||||
AbsolutePathBuf::try_from(path)?
|
||||
} else {
|
||||
cwd_uri.to_abs_path()?
|
||||
};
|
||||
Ok(TestEnv {
|
||||
environment,
|
||||
exec_server_url: Some(websocket_url),
|
||||
cwd,
|
||||
local_cwd_temp_dir: None,
|
||||
remote_container_name: remote_env.docker_container_name().map(str::to_owned),
|
||||
})
|
||||
}
|
||||
None => TestEnv::local().await,
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_exec_server_url() -> Result<String> {
|
||||
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")
|
||||
})?;
|
||||
let listen_url = listen_url.trim();
|
||||
if listen_url.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"{REMOTE_EXEC_SERVER_URL_ENV_VAR} must not be empty"
|
||||
));
|
||||
}
|
||||
Ok(listen_url.to_string())
|
||||
}
|
||||
|
||||
fn remote_test_instance_id() -> String {
|
||||
let instance = REMOTE_TEST_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
format!("{}-{instance}", std::process::id())
|
||||
}
|
||||
|
||||
fn docker_command_capture_stdout<const N: usize>(args: [&str; N]) -> Result<String> {
|
||||
let output = Command::new("docker")
|
||||
.args(args)
|
||||
.output()
|
||||
.with_context(|| format!("run docker {args:?}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"docker {:?} failed: stdout={} stderr={}",
|
||||
args,
|
||||
String::from_utf8_lossy(&output.stdout).trim(),
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
String::from_utf8(output.stdout).context("docker stdout must be utf-8")
|
||||
}
|
||||
|
||||
/// Non-default apply_patch model output shapes used by compatibility tests.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ApplyPatchModelOutput {
|
||||
@@ -515,7 +386,7 @@ impl TestCodexBuilder {
|
||||
let exec_server_url = self
|
||||
.exec_server_url
|
||||
.clone()
|
||||
.or_else(|| test_env.exec_server_url.clone());
|
||||
.or_else(|| test_env.exec_server_url().map(str::to_owned));
|
||||
#[cfg(target_os = "linux")]
|
||||
let codex_linux_sandbox_exe = Some(
|
||||
crate::find_codex_linux_sandbox_exe()
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use tempfile::TempDir;
|
||||
|
||||
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";
|
||||
const REMOTE_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_TEST_REMOTE_EXEC_SERVER_URL";
|
||||
static REMOTE_TEST_INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TestEnvironment {
|
||||
@@ -27,32 +41,154 @@ impl TestEnvironment {
|
||||
Self::Local | Self::WineExec => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remote_cwd(&self, instance_id: &str) -> Result<Option<LegacyAppPathString>> {
|
||||
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(LegacyAppPathString::from_path_uri(
|
||||
&path_uri,
|
||||
self.path_convention(),
|
||||
)?))
|
||||
}
|
||||
/// Owns the execution environment and workspace selected for an integration test.
|
||||
///
|
||||
/// Local tests receive a temporary local directory. Docker and Wine-exec tests receive a unique
|
||||
/// directory created through the configured remote exec server.
|
||||
#[derive(Debug)]
|
||||
pub struct TestExecutionEnvironment {
|
||||
environment: Environment,
|
||||
cwd: AbsolutePathBuf,
|
||||
environment_cwd: LegacyAppPathString,
|
||||
state: TestExecutionEnvironmentState,
|
||||
}
|
||||
|
||||
pub(crate) fn path_convention(&self) -> PathConvention {
|
||||
match self {
|
||||
Self::Local => PathConvention::native(),
|
||||
Self::Docker { .. } => PathConvention::Posix,
|
||||
Self::WineExec => PathConvention::Windows,
|
||||
impl TestExecutionEnvironment {
|
||||
pub async fn new() -> Result<Self> {
|
||||
match test_environment() {
|
||||
TestEnvironment::Local => Self::local().await,
|
||||
TestEnvironment::Docker { container_name } => {
|
||||
Self::remote(RemoteTestEnvironment::Docker { container_name }).await
|
||||
}
|
||||
TestEnvironment::WineExec => Self::remote(RemoteTestEnvironment::WineExec).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn remote(remote: RemoteTestEnvironment) -> Result<Self> {
|
||||
let websocket_url = remote_exec_server_url()?;
|
||||
let environment = Environment::create_for_tests(Some(websocket_url))?;
|
||||
let instance_id = remote_test_instance_id();
|
||||
let (cwd_uri, path_convention) = match &remote {
|
||||
RemoteTestEnvironment::Docker { .. } => (
|
||||
PathUri::parse(&format!("file:///tmp/codex-test-cwd-{instance_id}"))?,
|
||||
PathConvention::Posix,
|
||||
),
|
||||
RemoteTestEnvironment::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-test-cwd-{instance_id}"))?,
|
||||
PathConvention::Windows,
|
||||
)
|
||||
}
|
||||
};
|
||||
let environment_cwd = LegacyAppPathString::from_path_uri(&cwd_uri, path_convention)?;
|
||||
environment
|
||||
.get_filesystem()
|
||||
.create_directory(
|
||||
&cwd_uri,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
let cwd = match &remote {
|
||||
RemoteTestEnvironment::Docker { .. } => cwd_uri.to_abs_path()?,
|
||||
RemoteTestEnvironment::WineExec => {
|
||||
// TODO(anp): Change `Config::cwd` to `LegacyAppPathString` so this host-compatible
|
||||
// projection can be removed.
|
||||
let path = cwd_uri.to_url().to_file_path().map_err(|()| {
|
||||
anyhow!("remote test cwd URI cannot be projected onto the host: {cwd_uri}")
|
||||
})?;
|
||||
AbsolutePathBuf::try_from(path)?
|
||||
}
|
||||
};
|
||||
let state = match remote {
|
||||
RemoteTestEnvironment::Docker { container_name } => {
|
||||
TestExecutionEnvironmentState::Docker { container_name }
|
||||
}
|
||||
RemoteTestEnvironment::WineExec => TestExecutionEnvironmentState::WineExec,
|
||||
};
|
||||
Ok(Self {
|
||||
environment,
|
||||
cwd,
|
||||
environment_cwd,
|
||||
state,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn local() -> Result<Self> {
|
||||
let local_cwd_temp_dir = Arc::new(TempDir::new()?);
|
||||
let cwd = AbsolutePathBuf::from_absolute_path_checked(local_cwd_temp_dir.path())?;
|
||||
let environment = Environment::create_for_tests(/*exec_server_url*/ None)?;
|
||||
Ok(Self {
|
||||
environment,
|
||||
environment_cwd: LegacyAppPathString::from_abs_path(&cwd),
|
||||
cwd,
|
||||
state: TestExecutionEnvironmentState::Local {
|
||||
temp_dir: local_cwd_temp_dir,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn environment_id(&self) -> &'static str {
|
||||
match self.state {
|
||||
TestExecutionEnvironmentState::Local { .. } => LOCAL_ENVIRONMENT_ID,
|
||||
TestExecutionEnvironmentState::Docker { .. }
|
||||
| TestExecutionEnvironmentState::WineExec => REMOTE_ENVIRONMENT_ID,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
self.environment.exec_server_url()
|
||||
}
|
||||
|
||||
pub fn cwd(&self) -> &AbsolutePathBuf {
|
||||
&self.cwd
|
||||
}
|
||||
|
||||
pub fn environment_cwd(&self) -> &LegacyAppPathString {
|
||||
&self.environment_cwd
|
||||
}
|
||||
|
||||
pub fn environment(&self) -> &Environment {
|
||||
&self.environment
|
||||
}
|
||||
|
||||
pub(crate) fn local_cwd_temp_dir(&self) -> Option<Arc<TempDir>> {
|
||||
match &self.state {
|
||||
TestExecutionEnvironmentState::Local { temp_dir, .. } => Some(Arc::clone(temp_dir)),
|
||||
TestExecutionEnvironmentState::Docker { .. }
|
||||
| TestExecutionEnvironmentState::WineExec => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestExecutionEnvironment {
|
||||
fn drop(&mut self) {
|
||||
if let TestExecutionEnvironmentState::Docker { container_name } = &self.state {
|
||||
let script = format!("rm -rf {}", self.cwd.as_path().display());
|
||||
let _ = docker_command_capture_stdout(["exec", container_name, "sh", "-lc", &script]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TestExecutionEnvironmentState {
|
||||
Local { temp_dir: Arc<TempDir> },
|
||||
Docker { container_name: String },
|
||||
WineExec,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RemoteTestEnvironment {
|
||||
Docker { container_name: String },
|
||||
WineExec,
|
||||
}
|
||||
|
||||
pub async fn test_execution_environment() -> Result<TestExecutionEnvironment> {
|
||||
TestExecutionEnvironment::new().await
|
||||
}
|
||||
|
||||
pub fn test_environment() -> TestEnvironment {
|
||||
@@ -129,6 +265,40 @@ fn non_empty_utf8(name: &str, value: &OsStr) -> Result<String, String> {
|
||||
Ok(value.to_string())
|
||||
}
|
||||
|
||||
fn remote_exec_server_url() -> Result<String> {
|
||||
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")
|
||||
})?;
|
||||
let listen_url = listen_url.trim();
|
||||
if listen_url.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"{REMOTE_EXEC_SERVER_URL_ENV_VAR} must not be empty"
|
||||
));
|
||||
}
|
||||
Ok(listen_url.to_string())
|
||||
}
|
||||
|
||||
fn remote_test_instance_id() -> String {
|
||||
let instance = REMOTE_TEST_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
format!("{}-{instance}", std::process::id())
|
||||
}
|
||||
|
||||
fn docker_command_capture_stdout<const N: usize>(args: [&str; N]) -> Result<String> {
|
||||
let output = Command::new("docker")
|
||||
.args(args)
|
||||
.output()
|
||||
.with_context(|| format!("run docker {args:?}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"docker {:?} failed: stdout={} stderr={}",
|
||||
args,
|
||||
String::from_utf8_lossy(&output.stdout).trim(),
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
String::from_utf8(output.stdout).context("docker stdout must be utf-8")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "test_environment_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_exec_server::ExecOutputStream;
|
||||
use codex_exec_server::ExecParams;
|
||||
use codex_exec_server::ExecProcessEvent;
|
||||
use codex_exec_server::ProcessId;
|
||||
use core_test_support::TestEnvironment;
|
||||
use core_test_support::TestExecutionEnvironment;
|
||||
use core_test_support::test_environment;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const EXPECTED_EXEC_OUTPUT: &str = "hello from the selected executor";
|
||||
|
||||
#[tokio::test]
|
||||
async fn selected_execution_environment_runs_target_native_command() -> Result<()> {
|
||||
let execution = TestExecutionEnvironment::new().await?;
|
||||
let shell = execution.environment().info().await?.shell;
|
||||
let command = match test_environment() {
|
||||
TestEnvironment::WineExec => vec![
|
||||
"cmd.exe".to_string(),
|
||||
"/d".to_string(),
|
||||
"/c".to_string(),
|
||||
format!("echo {EXPECTED_EXEC_OUTPUT}"),
|
||||
],
|
||||
TestEnvironment::Local if cfg!(windows) => vec![
|
||||
"cmd.exe".to_string(),
|
||||
"/d".to_string(),
|
||||
"/c".to_string(),
|
||||
format!("echo {EXPECTED_EXEC_OUTPUT}"),
|
||||
],
|
||||
TestEnvironment::Local | TestEnvironment::Docker { .. } => vec![
|
||||
shell.path,
|
||||
"-lc".to_string(),
|
||||
format!("printf '{EXPECTED_EXEC_OUTPUT}'"),
|
||||
],
|
||||
};
|
||||
let cwd = execution
|
||||
.environment_cwd()
|
||||
.to_inferred_path_uri()
|
||||
.context("test environment cwd should be absolute")?;
|
||||
let started = execution
|
||||
.environment()
|
||||
.get_exec_backend()
|
||||
.start(ExecParams {
|
||||
process_id: ProcessId::from("test-execution-environment-smoke"),
|
||||
argv: command,
|
||||
cwd,
|
||||
env_policy: None,
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
})
|
||||
.await?;
|
||||
let mut events = started.process.subscribe_events();
|
||||
let mut output = Vec::new();
|
||||
let mut exit_code = None;
|
||||
loop {
|
||||
match timeout(Duration::from_secs(10), events.recv()).await?? {
|
||||
ExecProcessEvent::Output(chunk)
|
||||
if matches!(
|
||||
chunk.stream,
|
||||
ExecOutputStream::Stdout | ExecOutputStream::Pty
|
||||
) =>
|
||||
{
|
||||
output.extend(chunk.chunk.0);
|
||||
}
|
||||
ExecProcessEvent::Output(_) => {}
|
||||
ExecProcessEvent::Exited {
|
||||
exit_code: code, ..
|
||||
} => exit_code = Some(code),
|
||||
ExecProcessEvent::Closed { .. } => break,
|
||||
ExecProcessEvent::Failed(error) => bail!("test execution failed: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
let output = String::from_utf8(output)?;
|
||||
assert_eq!(output.trim_end_matches(['\r', '\n']), EXPECTED_EXEC_OUTPUT);
|
||||
assert_eq!(exit_code, Some(0));
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user