Compare commits

...

1 Commits

Author SHA1 Message Date
celia-oai
2f2700dfc8 changes 2026-02-22 00:45:35 -08:00
3 changed files with 175 additions and 89 deletions

View File

@@ -11,6 +11,7 @@ pub use auth_fixtures::ChatGptIdTokenClaims;
pub use auth_fixtures::encode_id_token;
pub use auth_fixtures::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCResponse;
use codex_utils_cargo_bin::find_resource;
pub use config::write_mock_responses_config_toml;
pub use core_test_support::format_with_current_shell;
pub use core_test_support::format_with_current_shell_display;
@@ -36,9 +37,93 @@ pub use rollout::create_fake_rollout_with_source;
pub use rollout::create_fake_rollout_with_text_elements;
pub use rollout::rollout_path;
use serde::de::DeserializeOwned;
use std::path::Path;
use std::path::PathBuf;
use tokio::process::Command;
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
let value = serde_json::to_value(response.result)?;
let codex_response = serde_json::from_value(value)?;
Ok(codex_response)
}
pub struct TestZshExecutable {
pub path: PathBuf,
dotslash_cache_dir: Option<PathBuf>,
}
impl TestZshExecutable {
pub async fn new_mcp_process(&self, codex_home: &Path) -> anyhow::Result<McpProcess> {
if let Some(dotslash_cache_dir) = &self.dotslash_cache_dir {
let dotslash_cache = dotslash_cache_dir.to_string_lossy().into_owned();
return McpProcess::new_with_env(
codex_home,
&[("DOTSLASH_CACHE", Some(&dotslash_cache))],
)
.await;
}
McpProcess::new(codex_home).await
}
}
pub async fn find_dotslash_test_zsh() -> anyhow::Result<TestZshExecutable> {
let zsh = find_resource!("../suite/zsh")?;
if !zsh.is_file() {
anyhow::bail!("zsh fork test fixture not found: {}", zsh.display());
}
let dotslash_cache_dir = create_dotslash_cache_dir()?;
let status = Command::new("dotslash")
.arg("--")
.arg("fetch")
.arg(&zsh)
.env("DOTSLASH_CACHE", &dotslash_cache_dir)
.status()
.await?;
if !status.success() {
anyhow::bail!("dotslash fetch failed for {}: {status}", zsh.display());
}
let zsh = TestZshExecutable {
path: zsh,
dotslash_cache_dir: Some(dotslash_cache_dir),
};
if !supports_exec_wrapper_intercept(&zsh) {
anyhow::bail!(
"zsh fork test fixture does not support EXEC_WRAPPER intercepts: {}",
zsh.path.display()
);
}
eprintln!("using zsh path for zsh-fork test: {}", zsh.path.display());
Ok(zsh)
}
fn create_dotslash_cache_dir() -> anyhow::Result<PathBuf> {
let timestamp_nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos();
let dotslash_cache_dir = std::env::temp_dir().join(format!(
"codex-zsh-dotslash-cache-{}-{timestamp_nanos}",
std::process::id()
));
std::fs::create_dir_all(&dotslash_cache_dir)?;
Ok(dotslash_cache_dir)
}
fn supports_exec_wrapper_intercept(zsh: &TestZshExecutable) -> bool {
let mut cmd = std::process::Command::new(&zsh.path);
cmd.arg("-fc")
.arg("/usr/bin/true")
.env("EXEC_WRAPPER", "/usr/bin/false");
if let Some(dotslash_cache_dir) = &zsh.dotslash_cache_dir {
cmd.env("DOTSLASH_CACHE", dotslash_cache_dir);
}
let status = cmd.status();
match status {
Ok(status) => !status.success(),
Err(_) => false,
}
}

View File

@@ -2,19 +2,20 @@
//
// Running these tests with the patched zsh fork:
//
// The suite uses `CODEX_TEST_ZSH_PATH` when set. Example:
// CODEX_TEST_ZSH_PATH="$HOME/.local/codex-zsh-77045ef/bin/zsh" \
// The suite uses `tests/suite/zsh` (a DotSlash-pinned fork).
//
// Run all zsh-fork tests:
// cargo test -p codex-app-server turn_start_zsh_fork -- --nocapture
//
// For a single test:
// CODEX_TEST_ZSH_PATH="$HOME/.local/codex-zsh-77045ef/bin/zsh" \
// cargo test -p codex-app-server turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2 -- --nocapture
// cargo test -p codex-app-server \
// turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2 -- --nocapture
use anyhow::Result;
use app_test_support::McpProcess;
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::find_dotslash_test_zsh;
use app_test_support::to_response;
use codex_app_server_protocol::CommandExecutionApprovalDecision;
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
@@ -57,12 +58,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let Some(zsh_path) = find_test_zsh_path() else {
eprintln!("skipping zsh fork test: no zsh executable found");
return Ok(());
};
eprintln!("using zsh path for zsh-fork test: {}", zsh_path.display());
let zsh = find_dotslash_test_zsh().await?;
let responses = vec![create_shell_command_sse_response(
vec!["echo".to_string(), "hi".to_string()],
None,
@@ -79,10 +75,10 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path,
&zsh.path,
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
let mut mcp = zsh.new_mcp_process(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -147,7 +143,6 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
};
assert_eq!(id, "call-zsh-fork");
assert_eq!(status, CommandExecutionStatus::InProgress);
assert!(command.starts_with(&zsh_path.display().to_string()));
assert!(command.contains(" -lc 'echo hi'"));
assert_eq!(cwd, workspace);
@@ -167,12 +162,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_decline_v2() -> Result<()> {
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let Some(zsh_path) = find_test_zsh_path() else {
eprintln!("skipping zsh fork decline test: no zsh executable found");
return Ok(());
};
eprintln!("using zsh path for zsh-fork test: {}", zsh_path.display());
let zsh = find_dotslash_test_zsh().await?;
let responses = vec![
create_shell_command_sse_response(
vec![
@@ -196,10 +186,10 @@ async fn turn_start_shell_zsh_fork_exec_approval_decline_v2() -> Result<()> {
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path,
&zsh.path,
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
let mut mcp = zsh.new_mcp_process(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -303,12 +293,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_cancel_v2() -> Result<()> {
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let Some(zsh_path) = find_test_zsh_path() else {
eprintln!("skipping zsh fork cancel test: no zsh executable found");
return Ok(());
};
eprintln!("using zsh path for zsh-fork test: {}", zsh_path.display());
let zsh = find_dotslash_test_zsh().await?;
let responses = vec![create_shell_command_sse_response(
vec![
"python3".to_string(),
@@ -329,10 +314,10 @@ async fn turn_start_shell_zsh_fork_exec_approval_cancel_v2() -> Result<()> {
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path,
&zsh.path,
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
let mut mcp = zsh.new_mcp_process(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -434,19 +419,7 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let Some(zsh_path) = find_test_zsh_path() else {
eprintln!("skipping zsh fork subcommand decline test: no zsh executable found");
return Ok(());
};
if !supports_exec_wrapper_intercept(&zsh_path) {
eprintln!(
"skipping zsh fork subcommand decline test: zsh does not support EXEC_WRAPPER intercepts ({})",
zsh_path.display()
);
return Ok(());
}
eprintln!("using zsh path for zsh-fork test: {}", zsh_path.display());
let zsh = find_dotslash_test_zsh().await?;
let tool_call_arguments = serde_json::to_string(&serde_json::json!({
"command": "/usr/bin/true && /usr/bin/true",
"workdir": serde_json::Value::Null,
@@ -471,10 +444,10 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path,
&zsh.path,
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
let mut mcp = zsh.new_mcp_process(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -639,47 +612,3 @@ stream_max_retries = 0
),
)
}
fn find_test_zsh_path() -> Option<std::path::PathBuf> {
if let Some(path) = std::env::var_os("CODEX_TEST_ZSH_PATH") {
let path = std::path::PathBuf::from(path);
if path.is_file() {
return Some(path);
}
panic!(
"CODEX_TEST_ZSH_PATH is set but is not a file: {}",
path.display()
);
}
for candidate in ["/bin/zsh", "/usr/bin/zsh"] {
let path = Path::new(candidate);
if path.is_file() {
return Some(path.to_path_buf());
}
}
let shell = std::env::var_os("SHELL")?;
let shell_path = std::path::PathBuf::from(shell);
if shell_path
.file_name()
.is_some_and(|file_name| file_name == "zsh")
&& shell_path.is_file()
{
return Some(shell_path);
}
None
}
fn supports_exec_wrapper_intercept(zsh_path: &Path) -> bool {
let status = std::process::Command::new(zsh_path)
.arg("-fc")
.arg("/usr/bin/true")
.env("EXEC_WRAPPER", "/usr/bin/false")
.status();
match status {
Ok(status) => !status.success(),
Err(_) => false,
}
}

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env dotslash
// This is an instance of the fork of zsh that we bundle with
// https://www.npmjs.com/package/@openai/codex-shell-tool-mcp.
// Fetching the prebuilt version via DotSlash makes it easier to write
// integration tests for the MCP server.
//
// TODO(mbolin): Currently, we use a .tgz artifact that includes binaries for
// multiple platforms, but we could save a bit of space by making arch-specific
// artifacts available in the GitHub releases and referencing those here.
{
"name": "codex-zsh",
"platforms": {
// macOS 13 builds (and therefore x86_64) were dropped in
// https://github.com/openai/codex/pull/7295, so we only provide an
// Apple Silicon build for now.
"macos-aarch64": {
"size": 53771483,
"hash": "blake3",
"digest": "ff664f63f5e1fa62762c9aff0aafa66cf196faf9b157f98ec98f59c152fc7bd3",
"format": "tar.gz",
"path": "package/vendor/aarch64-apple-darwin/zsh/macos-15/zsh",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.104.0/codex-shell-tool-mcp-npm-0.104.0.tgz"
},
{
"type": "github-release",
"repo": "openai/codex",
"tag": "rust-v0.104.0",
"name": "codex-shell-tool-mcp-npm-0.104.0.tgz"
}
]
},
"linux-x86_64": {
"size": 53771483,
"hash": "blake3",
"digest": "ff664f63f5e1fa62762c9aff0aafa66cf196faf9b157f98ec98f59c152fc7bd3",
"format": "tar.gz",
"path": "package/vendor/x86_64-unknown-linux-musl/zsh/ubuntu-24.04/zsh",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.104.0/codex-shell-tool-mcp-npm-0.104.0.tgz"
},
{
"type": "github-release",
"repo": "openai/codex",
"tag": "rust-v0.104.0",
"name": "codex-shell-tool-mcp-npm-0.104.0.tgz"
}
]
},
"linux-aarch64": {
"size": 53771483,
"hash": "blake3",
"digest": "ff664f63f5e1fa62762c9aff0aafa66cf196faf9b157f98ec98f59c152fc7bd3",
"format": "tar.gz",
"path": "package/vendor/aarch64-unknown-linux-musl/zsh/ubuntu-24.04/zsh",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.104.0/codex-shell-tool-mcp-npm-0.104.0.tgz"
},
{
"type": "github-release",
"repo": "openai/codex",
"tag": "rust-v0.104.0",
"name": "codex-shell-tool-mcp-npm-0.104.0.tgz"
}
]
}
}
}