mirror of
https://github.com/openai/codex.git
synced 2026-02-27 11:13:46 +00:00
Compare commits
1 Commits
codex/load
...
dev/cc/zsh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f2700dfc8 |
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
72
codex-rs/app-server/tests/suite/zsh
Executable file
72
codex-rs/app-server/tests/suite/zsh
Executable 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user