Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/app-server/tests/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Comment thread
anp-oai marked this conversation as resolved.
codex-features = { workspace = true }
codex-login = { workspace = true }
codex-models-manager = { workspace = true }
Expand Down
79 changes: 79 additions & 0 deletions codex-rs/app-server/tests/common/test_app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use tokio::process::ChildStdin;
use tokio::process::ChildStdout;

use anyhow::Context;
use anyhow::ensure;
use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::CancelLoginAccountParams;
use codex_app_server_protocol::ClientInfo;
Expand Down Expand Up @@ -107,11 +108,19 @@ 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_NOISE_AUTH_TOKEN_ENV_VAR;
use codex_exec_server::CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR;
use codex_exec_server::CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR;
use codex_exec_server::CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR;
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::test_codex::TestEnv;
use core_test_support::test_codex::test_env;
use tokio::process::Command;

pub struct TestAppServer {
Expand All @@ -124,6 +133,7 @@ pub struct TestAppServer {
stdin: Option<ChildStdin>,
stdout: BufReader<ChildStdout>,
pending_messages: VecDeque<JSONRPCMessage>,
auto_env: Option<TestEnv>,
}

pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
Expand All @@ -139,6 +149,59 @@ impl TestAppServer {
Self::new_with_env_and_args(codex_home, &[], &[DISABLE_PLUGIN_STARTUP_TASKS_ARG]).await
}

/// Starts an app server with the standard test environment and retains it
/// for the server's lifetime.
///
/// Local test runs explicitly remove `CODEX_EXEC_SERVER_URL`; Docker- and
/// Wine-backed runs set it to the remote fixture URL. Use
/// [`Self::auto_env_params`] or
/// [`Self::send_thread_start_request_with_auto_env`] to select the matching
/// target-native cwd in a thread. Because `environments.toml` overrides the
/// URL-based configuration, this helper rejects a `codex_home` containing
/// that file.
pub async fn new_with_auto_env(codex_home: &Path) -> anyhow::Result<Self> {
let environments_toml = codex_home.join("environments.toml");
ensure!(
!environments_toml
.try_exists()
.with_context(|| format!("check whether {} exists", environments_toml.display()))?,
"new_with_auto_env cannot be used when {} exists",
environments_toml.display()
);

let auto_env = test_env().await?;
// Noise registry configuration takes precedence over the URL-based
// provider, so clear inherited values to keep the selection hermetic.
let env_overrides = [
(
CODEX_EXEC_SERVER_URL_ENV_VAR,
auto_env.environment().exec_server_url(),
),
(CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR, None),
(CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR, None),
(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR, None),
(CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR, None),
];
let mut app_server = Self::new_with_env(codex_home, &env_overrides).await?;
app_server.auto_env = Some(auto_env);
Ok(app_server)
}

/// Returns app-server protocol parameters for the automatically selected
/// test environment. Returns an error unless this server was created with
/// [`Self::new_with_auto_env`].
pub fn auto_env_params(&self) -> anyhow::Result<TurnEnvironmentParams> {
let selection = self
.auto_env
.as_ref()
.context("auto environment is unavailable; use TestAppServer::new_with_auto_env")?
.selection();
Ok(TurnEnvironmentParams {
environment_id: selection.environment_id.clone(),
cwd: selection.cwd.clone().into(),
})
}

pub async fn new_without_managed_config(codex_home: &Path) -> anyhow::Result<Self> {
Self::new_with_env(codex_home, &[(DISABLE_MANAGED_CONFIG_ENV_VAR, Some("1"))]).await
}
Expand Down Expand Up @@ -273,6 +336,7 @@ impl TestAppServer {
stdin: Some(stdin),
stdout,
pending_messages: VecDeque::new(),
auto_env: None,
})
}

Expand Down Expand Up @@ -449,6 +513,21 @@ impl TestAppServer {
self.send_request("thread/start", params).await
}

/// Sends a `thread/start` request selecting the environment provisioned by
/// [`Self::new_with_auto_env`]. Returns an error if `params` already select
/// environments so the caller cannot accidentally override the fixture.
pub async fn send_thread_start_request_with_auto_env(
&mut self,
mut params: ThreadStartParams,
) -> anyhow::Result<i64> {
ensure!(
params.environments.is_none(),
"send_thread_start_request_with_auto_env requires params.environments to be omitted"
);
params.environments = Some(vec![self.auto_env_params()?]);
self.send_thread_start_request(params).await
}

/// Send a `thread/resume` JSON-RPC request.
pub async fn send_thread_resume_request(
&mut self,
Expand Down
151 changes: 151 additions & 0 deletions codex-rs/app-server/tests/suite/v2/auto_env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use anyhow::Result;
use app_test_support::TestAppServer;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_responses_server_sequence;
use app_test_support::create_shell_command_sse_response;
use app_test_support::to_response;
use app_test_support::write_mock_responses_config_toml;
use codex_app_server_protocol::CommandExecutionStatus;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::UserInput as V2UserInput;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::timeout;

const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);

#[tokio::test]
async fn thread_start_with_auto_env_uses_fixture_cwd() -> Result<()> {
let responses = vec![
create_shell_command_sse_response(
vec!["echo".to_string(), "auto-env-ok".to_string()],
/*workdir*/ None,
/*timeout_ms*/ None,
"cwd-call",
)?,
create_final_assistant_message_sse_response("done")?,
];
let server = create_mock_responses_server_sequence(responses).await;
let codex_home = TempDir::new()?;
write_mock_responses_config_toml(
codex_home.path(),
&server.uri(),
&BTreeMap::new(),
/*auto_compact_limit*/ 100_000,
/*requires_openai_auth*/ None,
"mock_provider",
"compact",
)?;

let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let expected_environment = mcp.auto_env_params()?;

let err = mcp
.send_thread_start_request_with_auto_env(ThreadStartParams {
environments: Some(Vec::new()),
..Default::default()
})
.await
.expect_err("the auto-env helper should reject caller-supplied environments");
assert_eq!(
err.to_string(),
"send_thread_start_request_with_auto_env requires params.environments to be omitted"
);

let request_id = mcp
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(response)?;

let request_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
input: vec![V2UserInput::Text {
text: "report the current directory".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
let _: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;

let command = timeout(DEFAULT_READ_TIMEOUT, async {
loop {
let notification = mcp
.read_stream_until_notification_message("item/completed")
.await?;
let completed: ItemCompletedNotification = serde_json::from_value(
notification
.params
.expect("item/completed params must be present"),
)?;
if let ThreadItem::CommandExecution { .. } = completed.item {
return Ok::<ThreadItem, anyhow::Error>(completed.item);
}
}
})
.await??;
let ThreadItem::CommandExecution {
cwd,
status,
exit_code,
..
} = command
else {
unreachable!("loop returns only command execution items");
};
assert_eq!(
(cwd, status, exit_code),
(
expected_environment.cwd,
CommandExecutionStatus::Completed,
Some(0)
)
);

timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;

Ok(())
}

#[tokio::test]
async fn auto_env_rejects_explicit_environment_config() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(codex_home.path().join("environments.toml"), "")?;

let result = TestAppServer::new_with_auto_env(codex_home.path()).await;
let Err(err) = result else {
anyhow::bail!("auto-env construction unexpectedly succeeded");
};
assert_eq!(
err.to_string(),
format!(
"new_with_auto_env cannot be used when {} exists",
codex_home.path().join("environments.toml").display()
)
);

Ok(())
}
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod account;
mod analytics;
mod app_list;
mod attestation;
mod auto_env;
mod client_metadata;
mod collaboration_mode_list;
#[cfg(unix)]
Expand Down
13 changes: 13 additions & 0 deletions codex-rs/core/tests/common/test_codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ pub struct TestEnv {
environment: codex_exec_server::Environment,
exec_server_url: Option<String>,
cwd: AbsolutePathBuf,
selection: TurnEnvironmentSelection,
local_cwd_temp_dir: Option<Arc<TempDir>>,
remote_container_name: Option<String>,
}
Expand All @@ -131,10 +132,12 @@ impl TestEnv {
let cwd = local_cwd_temp_dir.abs();
let environment =
codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None)?;
let selection = local(cwd.clone());
Ok(Self {
environment,
exec_server_url: None,
cwd,
selection,
local_cwd_temp_dir: Some(local_cwd_temp_dir),
remote_container_name: None,
})
Expand All @@ -148,6 +151,11 @@ impl TestEnv {
&self.environment
}

/// Returns the environment and target-native cwd selected by the test harness.
pub fn selection(&self) -> &TurnEnvironmentSelection {
&self.selection
}

fn local_cwd_temp_dir(&self) -> Option<Arc<TempDir>> {
self.local_cwd_temp_dir.clone()
}
Expand Down Expand Up @@ -180,6 +188,10 @@ pub async fn test_env() -> Result<TestEnv> {
/*sandbox*/ None,
)
.await?;
let selection = TurnEnvironmentSelection {
environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
};
let cwd = if remote_env == TestEnvironment::WineExec {
// TODO(anp): Convert `Config::cwd` to `LegacyAppPathString` and remove this
// compatibility projection.
Expand All @@ -198,6 +210,7 @@ pub async fn test_env() -> Result<TestEnv> {
environment,
exec_server_url: Some(websocket_url),
cwd,
selection,
local_cwd_temp_dir: None,
remote_container_name: remote_env.docker_container_name().map(str::to_owned),
})
Expand Down
3 changes: 1 addition & 2 deletions codex-rs/core/tests/suite/remote_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,7 @@ async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> {
let test_env = test_env().await?;
let file_system = test_env.environment().get_filesystem();

let file_path_abs = test_env.cwd().join("remote-test-env-ok");
let file_path_uri = PathUri::from_host_native_path(&file_path_abs)?;
let file_path_uri = test_env.selection().cwd.join("remote-test-env-ok")?;
let payload = b"remote-test-env-ok".to_vec();

file_system
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/exec-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ pub use codex_file_system::FileSystemResult;
pub use codex_file_system::FileSystemSandboxContext;
pub use codex_file_system::ReadDirectoryEntry;
pub use codex_file_system::RemoveOptions;
pub use environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
pub use environment::Environment;
pub use environment::EnvironmentManager;
Expand Down
Loading