From 28dcdb566ad0052c616b40efda1ad852cbb04970 Mon Sep 17 00:00:00 2001 From: Robby He <448523760@qq.com> Date: Fri, 5 Dec 2025 06:56:58 +0800 Subject: [PATCH 1/4] Fix `handle_shortcut_overlay_key` for cross-platform consistency (#7583) **Summary** - Shortcut toggle using `?` in `handle_shortcut_overlay_key` fails to trigger on some platforms (notably Windows). Current match requires `KeyCode::Char('?')` with `KeyModifiers::NONE`. Some terminals set `SHIFT` when producing `?` (since it is typically `Shift + /`), so the strict `NONE` check prevents toggling. **Impact** - On Windows consoles/terminals, pressing `?` with an empty composer often does nothing, leading to inconsistent UX compared to macOS/Linux. **Root Cause** - Crossterm/terminal backends report modifiers inconsistently across platforms. Generating `?` may include `SHIFT`. The code enforces `modifiers == NONE`, so valid `?` presses with `SHIFT` are ignored. AltGr keyboards may also surface as `ALT`. **Repro Steps** - Open the TUI, ensure the composer is empty. - Press `?`. - Expected: Shortcut overlay toggles. - Actual (Windows frequently): No toggle occurs. **Fix Options** - Option 1 (preferred): Accept `?` regardless of `SHIFT`, but reject `CONTROL` and `ALT`. - Rationale: Keeps behavior consistent across platforms with minimal code change. - Example change: - Before: matching `KeyModifiers::NONE` only. - After: allow `SHIFT`, disallow `CONTROL | ALT`. - Suggested condition: ```rust let toggles = matches!(key_event.code, KeyCode::Char('?')) && !key_event.modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) && self.is_empty(); ``` - Option 2: Platform-specific handling (Windows vs non-Windows). - Implement two variants or conditional branches using `#[cfg(target_os = "windows")]`. - On Windows, accept `?` with `SHIFT`; on other platforms, retain current behavior. - Trade-off: Higher maintenance burden and code divergence for limited benefit. --- close #5495 --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 4eeeb4bcee..4deb5125c1 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1504,14 +1504,9 @@ impl ChatComposer { return false; } - let toggles = matches!( - key_event, - KeyEvent { - code: KeyCode::Char('?'), - modifiers: KeyModifiers::NONE, - .. - } if self.is_empty() - ); + let toggles = matches!(key_event.code, KeyCode::Char('?')) + && !has_ctrl_or_alt(key_event.modifiers) + && self.is_empty(); if !toggles { return false; From 0972cd940422828ac9cc9754d1ff07691ba07545 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 4 Dec 2025 15:13:27 -0800 Subject: [PATCH 2/4] chore: refactor to move Arc concern outside exec_policy_for (#7615) The caller should decide whether wrapping the policy in `Arc` is necessary. This should make https://github.com/openai/codex/pull/7609 a bit smoother. - `exec_policy_for()` -> `load_exec_policy_for_features()` - introduce `load_exec_policy()` that does not take `Features` as an arg - both return `Result` instead of Result>, ExecPolicyError>` This simplifies the tests as they have no need for `Arc`. --- codex-rs/core/src/codex.rs | 4 +++- codex-rs/core/src/exec_policy.rs | 33 ++++++++++++++------------------ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index abd5116f2a..436021a9d2 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -11,6 +11,7 @@ use crate::compact; use crate::compact::run_inline_auto_compact_task; use crate::compact::should_use_remote_compact_task; use crate::compact_remote::run_inline_remote_auto_compact_task; +use crate::exec_policy::load_exec_policy_for_features; use crate::features::Feature; use crate::features::Features; use crate::openai_models::models_manager::ModelsManager; @@ -174,9 +175,10 @@ impl Codex { let user_instructions = get_user_instructions(&config).await; - let exec_policy = crate::exec_policy::exec_policy_for(&config.features, &config.codex_home) + let exec_policy = load_exec_policy_for_features(&config.features, &config.codex_home) .await .map_err(|err| CodexErr::Fatal(format!("failed to load execpolicy: {err}")))?; + let exec_policy = Arc::new(RwLock::new(exec_policy)); let config = Arc::new(config); diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 1bbe60ff11..2d1c2efe5e 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -73,14 +73,18 @@ pub enum ExecPolicyUpdateError { FeatureDisabled, } -pub(crate) async fn exec_policy_for( +pub(crate) async fn load_exec_policy_for_features( features: &Features, codex_home: &Path, -) -> Result>, ExecPolicyError> { +) -> Result { if !features.enabled(Feature::ExecPolicy) { - return Ok(Arc::new(RwLock::new(Policy::empty()))); + Ok(Policy::empty()) + } else { + load_exec_policy(codex_home).await } +} +pub async fn load_exec_policy(codex_home: &Path) -> Result { let policy_dir = codex_home.join(POLICY_DIR_NAME); let policy_paths = collect_policy_files(&policy_dir).await?; @@ -102,7 +106,7 @@ pub(crate) async fn exec_policy_for( })?; } - let policy = Arc::new(RwLock::new(parser.build())); + let policy = parser.build(); tracing::debug!( "loaded execpolicy from {} files in {}", policy_paths.len(), @@ -306,7 +310,7 @@ mod tests { features.disable(Feature::ExecPolicy); let temp_dir = tempdir().expect("create temp dir"); - let policy = exec_policy_for(&features, temp_dir.path()) + let policy = load_exec_policy_for_features(&features, temp_dir.path()) .await .expect("policy result"); @@ -319,10 +323,7 @@ mod tests { decision: Decision::Allow }], }, - policy - .read() - .await - .check_multiple(commands.iter(), &|_| Decision::Allow) + policy.check_multiple(commands.iter(), &|_| Decision::Allow) ); assert!(!temp_dir.path().join(POLICY_DIR_NAME).exists()); } @@ -350,7 +351,7 @@ mod tests { ) .expect("write policy file"); - let policy = exec_policy_for(&Features::with_defaults(), temp_dir.path()) + let policy = load_exec_policy(temp_dir.path()) .await .expect("policy result"); let command = [vec!["rm".to_string()]]; @@ -362,10 +363,7 @@ mod tests { decision: Decision::Forbidden }], }, - policy - .read() - .await - .check_multiple(command.iter(), &|_| Decision::Allow) + policy.check_multiple(command.iter(), &|_| Decision::Allow) ); } @@ -378,7 +376,7 @@ mod tests { ) .expect("write policy file"); - let policy = exec_policy_for(&Features::with_defaults(), temp_dir.path()) + let policy = load_exec_policy(temp_dir.path()) .await .expect("policy result"); let command = [vec!["ls".to_string()]]; @@ -390,10 +388,7 @@ mod tests { decision: Decision::Allow }], }, - policy - .read() - .await - .check_multiple(command.iter(), &|_| Decision::Allow) + policy.check_multiple(command.iter(), &|_| Decision::Allow) ); } From 073a8533b83f10cf2c3aa3678ce11fd317587b50 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Thu, 4 Dec 2025 16:20:54 -0800 Subject: [PATCH 3/4] chore(apply-patch) scenarios for e2e testing (#7567) ## Summary This PR introduces an End to End test suite for apply-patch, so we can easily validate behavior against other implementations as well. ## Testing - [x] These are tests --- .../tests/fixtures/scenarios/.gitattributes | 1 + .../scenarios/001_add_file/expected/bar.md | 1 + .../fixtures/scenarios/001_add_file/patch.txt | 4 + .../expected/modify.txt | 2 + .../expected/nested/new.txt | 1 + .../002_multiple_operations/input/delete.txt | 1 + .../002_multiple_operations/input/modify.txt | 2 + .../002_multiple_operations/patch.txt | 9 ++ .../003_multiple_chunks/expected/multi.txt | 4 + .../003_multiple_chunks/input/multi.txt | 4 + .../scenarios/003_multiple_chunks/patch.txt | 9 ++ .../expected/old/other.txt | 1 + .../expected/renamed/dir/name.txt | 1 + .../input/old/name.txt | 1 + .../input/old/other.txt | 1 + .../004_move_to_new_directory/patch.txt | 7 ++ .../005_rejects_empty_patch/patch.txt | 2 + .../expected/modify.txt | 2 + .../input/modify.txt | 2 + .../006_rejects_missing_context/patch.txt | 6 + .../007_rejects_missing_file_delete/patch.txt | 3 + .../008_rejects_empty_update_hunk/patch.txt | 3 + .../patch.txt | 6 + .../expected/old/other.txt | 1 + .../expected/renamed/dir/name.txt | 1 + .../input/old/name.txt | 1 + .../input/old/other.txt | 1 + .../input/renamed/dir/name.txt | 1 + .../patch.txt | 7 ++ .../expected/duplicate.txt | 1 + .../input/duplicate.txt | 1 + .../patch.txt | 4 + .../012_delete_directory_fails/patch.txt | 3 + .../013_rejects_invalid_hunk_header/patch.txt | 3 + .../expected/no_newline.txt | 2 + .../input/no_newline.txt | 1 + .../patch.txt | 7 ++ .../expected/created.txt | 1 + .../patch.txt | 8 ++ .../expected/input.txt | 4 + .../input/input.txt | 2 + .../016_pure_addition_update_chunk/patch.txt | 6 + .../expected/foo.txt | 1 + .../input/foo.txt | 1 + .../patch.txt | 6 + .../expected/file.txt | 1 + .../input/file.txt | 1 + .../patch.txt | 6 + .../tests/fixtures/scenarios/README.md | 18 +++ codex-rs/apply-patch/tests/suite/mod.rs | 1 + codex-rs/apply-patch/tests/suite/scenarios.rs | 114 ++++++++++++++++++ 51 files changed, 277 insertions(+) create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/.gitattributes create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/001_add_file/expected/bar.md create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/001_add_file/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/expected/modify.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/expected/nested/new.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/input/delete.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/input/modify.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/expected/multi.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/input/multi.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/expected/old/other.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/expected/renamed/dir/name.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/input/old/name.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/input/old/other.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/005_rejects_empty_patch/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/expected/modify.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/input/modify.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/007_rejects_missing_file_delete/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/008_rejects_empty_update_hunk/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/009_requires_existing_file_for_update/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/expected/old/other.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/expected/renamed/dir/name.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/old/name.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/old/other.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/renamed/dir/name.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/expected/duplicate.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/input/duplicate.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/012_delete_directory_fails/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/013_rejects_invalid_hunk_header/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/expected/no_newline.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/input/no_newline.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/015_failure_after_partial_success_leaves_changes/expected/created.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/015_failure_after_partial_success_leaves_changes/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/expected/input.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/input/input.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/expected/foo.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/input/foo.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/expected/file.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/input/file.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/patch.txt create mode 100644 codex-rs/apply-patch/tests/fixtures/scenarios/README.md create mode 100644 codex-rs/apply-patch/tests/suite/scenarios.rs diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/.gitattributes b/codex-rs/apply-patch/tests/fixtures/scenarios/.gitattributes new file mode 100644 index 0000000000..a42a20ddc5 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/.gitattributes @@ -0,0 +1 @@ +** text eol=lf diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/001_add_file/expected/bar.md b/codex-rs/apply-patch/tests/fixtures/scenarios/001_add_file/expected/bar.md new file mode 100644 index 0000000000..6dfa057f0d --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/001_add_file/expected/bar.md @@ -0,0 +1 @@ +This is a new file diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/001_add_file/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/001_add_file/patch.txt new file mode 100644 index 0000000000..37735b2a46 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/001_add_file/patch.txt @@ -0,0 +1,4 @@ +*** Begin Patch +*** Add File: bar.md ++This is a new file +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/expected/modify.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/expected/modify.txt new file mode 100644 index 0000000000..1b2ee3e566 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/expected/modify.txt @@ -0,0 +1,2 @@ +line1 +changed diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/expected/nested/new.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/expected/nested/new.txt new file mode 100644 index 0000000000..3151666398 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/expected/nested/new.txt @@ -0,0 +1 @@ +created diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/input/delete.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/input/delete.txt new file mode 100644 index 0000000000..6e263abce1 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/input/delete.txt @@ -0,0 +1 @@ +obsolete diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/input/modify.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/input/modify.txt new file mode 100644 index 0000000000..c0d0fb45c3 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/input/modify.txt @@ -0,0 +1,2 @@ +line1 +line2 diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/patch.txt new file mode 100644 index 0000000000..673dec2f78 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/002_multiple_operations/patch.txt @@ -0,0 +1,9 @@ +*** Begin Patch +*** Add File: nested/new.txt ++created +*** Delete File: delete.txt +*** Update File: modify.txt +@@ +-line2 ++changed +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/expected/multi.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/expected/multi.txt new file mode 100644 index 0000000000..9054a72916 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/expected/multi.txt @@ -0,0 +1,4 @@ +line1 +changed2 +line3 +changed4 diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/input/multi.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/input/multi.txt new file mode 100644 index 0000000000..84275f9939 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/input/multi.txt @@ -0,0 +1,4 @@ +line1 +line2 +line3 +line4 diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/patch.txt new file mode 100644 index 0000000000..45733c714b --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/003_multiple_chunks/patch.txt @@ -0,0 +1,9 @@ +*** Begin Patch +*** Update File: multi.txt +@@ +-line2 ++changed2 +@@ +-line4 ++changed4 +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/expected/old/other.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/expected/old/other.txt new file mode 100644 index 0000000000..b61039d3df --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/expected/old/other.txt @@ -0,0 +1 @@ +unrelated file diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/expected/renamed/dir/name.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/expected/renamed/dir/name.txt new file mode 100644 index 0000000000..b66ba06d31 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/expected/renamed/dir/name.txt @@ -0,0 +1 @@ +new content diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/input/old/name.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/input/old/name.txt new file mode 100644 index 0000000000..33194a0a6f --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/input/old/name.txt @@ -0,0 +1 @@ +old content diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/input/old/other.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/input/old/other.txt new file mode 100644 index 0000000000..b61039d3df --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/input/old/other.txt @@ -0,0 +1 @@ +unrelated file diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/patch.txt new file mode 100644 index 0000000000..5e2d723a2b --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/004_move_to_new_directory/patch.txt @@ -0,0 +1,7 @@ +*** Begin Patch +*** Update File: old/name.txt +*** Move to: renamed/dir/name.txt +@@ +-old content ++new content +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/005_rejects_empty_patch/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/005_rejects_empty_patch/patch.txt new file mode 100644 index 0000000000..4fcfecbbc7 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/005_rejects_empty_patch/patch.txt @@ -0,0 +1,2 @@ +*** Begin Patch +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/expected/modify.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/expected/modify.txt new file mode 100644 index 0000000000..c0d0fb45c3 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/expected/modify.txt @@ -0,0 +1,2 @@ +line1 +line2 diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/input/modify.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/input/modify.txt new file mode 100644 index 0000000000..c0d0fb45c3 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/input/modify.txt @@ -0,0 +1,2 @@ +line1 +line2 diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/patch.txt new file mode 100644 index 0000000000..488438b12b --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/006_rejects_missing_context/patch.txt @@ -0,0 +1,6 @@ +*** Begin Patch +*** Update File: modify.txt +@@ +-missing ++changed +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/007_rejects_missing_file_delete/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/007_rejects_missing_file_delete/patch.txt new file mode 100644 index 0000000000..6f95531db3 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/007_rejects_missing_file_delete/patch.txt @@ -0,0 +1,3 @@ +*** Begin Patch +*** Delete File: missing.txt +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/008_rejects_empty_update_hunk/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/008_rejects_empty_update_hunk/patch.txt new file mode 100644 index 0000000000..d7596a362b --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/008_rejects_empty_update_hunk/patch.txt @@ -0,0 +1,3 @@ +*** Begin Patch +*** Update File: foo.txt +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/009_requires_existing_file_for_update/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/009_requires_existing_file_for_update/patch.txt new file mode 100644 index 0000000000..a7de4f24c5 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/009_requires_existing_file_for_update/patch.txt @@ -0,0 +1,6 @@ +*** Begin Patch +*** Update File: missing.txt +@@ +-old ++new +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/expected/old/other.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/expected/old/other.txt new file mode 100644 index 0000000000..b61039d3df --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/expected/old/other.txt @@ -0,0 +1 @@ +unrelated file diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/expected/renamed/dir/name.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/expected/renamed/dir/name.txt new file mode 100644 index 0000000000..3e757656cf --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/expected/renamed/dir/name.txt @@ -0,0 +1 @@ +new diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/old/name.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/old/name.txt new file mode 100644 index 0000000000..3940df7cd8 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/old/name.txt @@ -0,0 +1 @@ +from diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/old/other.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/old/other.txt new file mode 100644 index 0000000000..b61039d3df --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/old/other.txt @@ -0,0 +1 @@ +unrelated file diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/renamed/dir/name.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/renamed/dir/name.txt new file mode 100644 index 0000000000..cbaf024e5e --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/input/renamed/dir/name.txt @@ -0,0 +1 @@ +existing diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/patch.txt new file mode 100644 index 0000000000..c45ce6d782 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/010_move_overwrites_existing_destination/patch.txt @@ -0,0 +1,7 @@ +*** Begin Patch +*** Update File: old/name.txt +*** Move to: renamed/dir/name.txt +@@ +-from ++new +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/expected/duplicate.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/expected/duplicate.txt new file mode 100644 index 0000000000..b66ba06d31 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/expected/duplicate.txt @@ -0,0 +1 @@ +new content diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/input/duplicate.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/input/duplicate.txt new file mode 100644 index 0000000000..33194a0a6f --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/input/duplicate.txt @@ -0,0 +1 @@ +old content diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/patch.txt new file mode 100644 index 0000000000..bad9cf3fde --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/011_add_overwrites_existing_file/patch.txt @@ -0,0 +1,4 @@ +*** Begin Patch +*** Add File: duplicate.txt ++new content +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/012_delete_directory_fails/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/012_delete_directory_fails/patch.txt new file mode 100644 index 0000000000..a10bcd9ea9 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/012_delete_directory_fails/patch.txt @@ -0,0 +1,3 @@ +*** Begin Patch +*** Delete File: dir +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/013_rejects_invalid_hunk_header/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/013_rejects_invalid_hunk_header/patch.txt new file mode 100644 index 0000000000..b35d7207d7 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/013_rejects_invalid_hunk_header/patch.txt @@ -0,0 +1,3 @@ +*** Begin Patch +*** Frobnicate File: foo +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/expected/no_newline.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/expected/no_newline.txt new file mode 100644 index 0000000000..06fcdd77c9 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/expected/no_newline.txt @@ -0,0 +1,2 @@ +first line +second line diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/input/no_newline.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/input/no_newline.txt new file mode 100644 index 0000000000..a6e09874b5 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/input/no_newline.txt @@ -0,0 +1 @@ +no newline at end diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/patch.txt new file mode 100644 index 0000000000..4ed5818eb1 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/014_update_file_appends_trailing_newline/patch.txt @@ -0,0 +1,7 @@ +*** Begin Patch +*** Update File: no_newline.txt +@@ +-no newline at end ++first line ++second line +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/015_failure_after_partial_success_leaves_changes/expected/created.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/015_failure_after_partial_success_leaves_changes/expected/created.txt new file mode 100644 index 0000000000..ce01362503 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/015_failure_after_partial_success_leaves_changes/expected/created.txt @@ -0,0 +1 @@ +hello diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/015_failure_after_partial_success_leaves_changes/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/015_failure_after_partial_success_leaves_changes/patch.txt new file mode 100644 index 0000000000..a6e9709d1f --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/015_failure_after_partial_success_leaves_changes/patch.txt @@ -0,0 +1,8 @@ +*** Begin Patch +*** Add File: created.txt ++hello +*** Update File: missing.txt +@@ +-old ++new +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/expected/input.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/expected/input.txt new file mode 100644 index 0000000000..f6d6f0bef8 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/expected/input.txt @@ -0,0 +1,4 @@ +line1 +line2 +added line 1 +added line 2 diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/input/input.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/input/input.txt new file mode 100644 index 0000000000..c0d0fb45c3 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/input/input.txt @@ -0,0 +1,2 @@ +line1 +line2 diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/patch.txt new file mode 100644 index 0000000000..56337549f9 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/016_pure_addition_update_chunk/patch.txt @@ -0,0 +1,6 @@ +*** Begin Patch +*** Update File: input.txt +@@ ++added line 1 ++added line 2 +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/expected/foo.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/expected/foo.txt new file mode 100644 index 0000000000..3e757656cf --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/expected/foo.txt @@ -0,0 +1 @@ +new diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/input/foo.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/input/foo.txt new file mode 100644 index 0000000000..3367afdbbf --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/input/foo.txt @@ -0,0 +1 @@ +old diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/patch.txt new file mode 100644 index 0000000000..21e6c1958d --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/017_whitespace_padded_hunk_header/patch.txt @@ -0,0 +1,6 @@ +*** Begin Patch + *** Update File: foo.txt +@@ +-old ++new +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/expected/file.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/expected/file.txt new file mode 100644 index 0000000000..f719efd430 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/expected/file.txt @@ -0,0 +1 @@ +two diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/input/file.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/input/file.txt new file mode 100644 index 0000000000..5626abf0f7 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/input/file.txt @@ -0,0 +1 @@ +one diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/patch.txt b/codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/patch.txt new file mode 100644 index 0000000000..2648721797 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/018_whitespace_padded_patch_markers/patch.txt @@ -0,0 +1,6 @@ + *** Begin Patch +*** Update File: file.txt +@@ +-one ++two +*** End Patch diff --git a/codex-rs/apply-patch/tests/fixtures/scenarios/README.md b/codex-rs/apply-patch/tests/fixtures/scenarios/README.md new file mode 100644 index 0000000000..65d1fbe2e4 --- /dev/null +++ b/codex-rs/apply-patch/tests/fixtures/scenarios/README.md @@ -0,0 +1,18 @@ +# Overview +This directory is a collection of end to end tests for the apply-patch specification, meant to be easily portable to other languages or platforms. + + +# Specification +Each test case is one directory, composed of input state (input/), the patch operation (patch.txt), and the expected final state (expected/). This structure is designed to keep tests simple (i.e. test exactly one patch at a time) while still providing enough flexibility to test any given operation across files. + +Here's what this would look like for a simple test apply-patch test case to create a new file: + +``` +001_add/ + input/ + foo.md + expected/ + foo.md + bar.md + patch.txt +``` diff --git a/codex-rs/apply-patch/tests/suite/mod.rs b/codex-rs/apply-patch/tests/suite/mod.rs index 882c5a6ffd..7d54de85ad 100644 --- a/codex-rs/apply-patch/tests/suite/mod.rs +++ b/codex-rs/apply-patch/tests/suite/mod.rs @@ -1,3 +1,4 @@ mod cli; +mod scenarios; #[cfg(not(target_os = "windows"))] mod tool; diff --git a/codex-rs/apply-patch/tests/suite/scenarios.rs b/codex-rs/apply-patch/tests/suite/scenarios.rs new file mode 100644 index 0000000000..4b3eb3c84a --- /dev/null +++ b/codex-rs/apply-patch/tests/suite/scenarios.rs @@ -0,0 +1,114 @@ +use assert_cmd::prelude::*; +use pretty_assertions::assert_eq; +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use tempfile::tempdir; + +#[test] +fn test_apply_patch_scenarios() -> anyhow::Result<()> { + for scenario in fs::read_dir("tests/fixtures/scenarios")? { + let scenario = scenario?; + let path = scenario.path(); + if path.is_dir() { + run_apply_patch_scenario(&path)?; + } + } + Ok(()) +} + +/// Reads a scenario directory, copies the input files to a temporary directory, runs apply-patch, +/// and asserts that the final state matches the expected state exactly. +fn run_apply_patch_scenario(dir: &Path) -> anyhow::Result<()> { + let tmp = tempdir()?; + + // Copy the input files to the temporary directory + let input_dir = dir.join("input"); + if input_dir.is_dir() { + copy_dir_recursive(&input_dir, tmp.path())?; + } + + // Read the patch.txt file + let patch = fs::read_to_string(dir.join("patch.txt"))?; + + // Run apply_patch in the temporary directory. We intentionally do not assert + // on the exit status here; the scenarios are specified purely in terms of + // final filesystem state, which we compare below. + Command::cargo_bin("apply_patch")? + .arg(patch) + .current_dir(tmp.path()) + .output()?; + + // Assert that the final state matches the expected state exactly + let expected_dir = dir.join("expected"); + let expected_snapshot = snapshot_dir(&expected_dir)?; + let actual_snapshot = snapshot_dir(tmp.path())?; + + assert_eq!( + actual_snapshot, + expected_snapshot, + "Scenario {} did not match expected final state", + dir.display() + ); + + Ok(()) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Entry { + File(Vec), + Dir, +} + +fn snapshot_dir(root: &Path) -> anyhow::Result> { + let mut entries = BTreeMap::new(); + if root.is_dir() { + snapshot_dir_recursive(root, root, &mut entries)?; + } + Ok(entries) +} + +fn snapshot_dir_recursive( + base: &Path, + dir: &Path, + entries: &mut BTreeMap, +) -> anyhow::Result<()> { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + let Some(stripped) = path.strip_prefix(base).ok() else { + continue; + }; + let rel = stripped.to_path_buf(); + let file_type = entry.file_type()?; + if file_type.is_dir() { + entries.insert(rel.clone(), Entry::Dir); + snapshot_dir_recursive(base, &path, entries)?; + } else if file_type.is_file() { + let contents = fs::read(&path)?; + entries.insert(rel, Entry::File(contents)); + } + } + Ok(()) +} + +fn copy_dir_recursive(src: &Path, dst: &Path) -> anyhow::Result<()> { + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let file_type = entry.file_type()?; + let dest_path = dst.join(entry.file_name()); + if file_type.is_dir() { + fs::create_dir_all(&dest_path)?; + copy_dir_recursive(&path, &dest_path)?; + } else if file_type.is_file() { + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent)?; + } + fs::copy(&path, &dest_path)?; + } + } + Ok(()) +} From 8eae62769a1352999be5960073407ef3974c1773 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 4 Dec 2025 16:41:00 -0800 Subject: [PATCH 4/4] fix: add test that verifies that codex-exec-mcp-server starts up --- codex-rs/Cargo.lock | 1 + codex-rs/exec-server/Cargo.toml | 5 +- codex-rs/exec-server/tests/all.rs | 3 + .../exec-server/tests/suite/auto_approve.rs | 86 +++++++++++++++++++ codex-rs/exec-server/tests/suite/bash | 25 ++++++ codex-rs/exec-server/tests/suite/mod.rs | 3 + 6 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 codex-rs/exec-server/tests/all.rs create mode 100644 codex-rs/exec-server/tests/suite/auto_approve.rs create mode 100755 codex-rs/exec-server/tests/suite/bash create mode 100644 codex-rs/exec-server/tests/suite/mod.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index c3b6c27bee..fb6e0260b1 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1253,6 +1253,7 @@ name = "codex-exec-server" version = "0.0.0" dependencies = [ "anyhow", + "assert_cmd", "async-trait", "clap", "codex-core", diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 5f8032595e..84b2a9bab6 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "codex-exec-server" -version.workspace = true edition.workspace = true license.workspace = true +name = "codex-exec-server" +version.workspace = true [[bin]] name = "codex-execve-wrapper" @@ -55,5 +55,6 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } [dev-dependencies] +assert_cmd = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/exec-server/tests/all.rs b/codex-rs/exec-server/tests/all.rs new file mode 100644 index 0000000000..7e136e4cce --- /dev/null +++ b/codex-rs/exec-server/tests/all.rs @@ -0,0 +1,3 @@ +// Single integration test binary that aggregates all test modules. +// The submodules live in `tests/suite/`. +mod suite; diff --git a/codex-rs/exec-server/tests/suite/auto_approve.rs b/codex-rs/exec-server/tests/suite/auto_approve.rs new file mode 100644 index 0000000000..93e0d0eb2c --- /dev/null +++ b/codex-rs/exec-server/tests/suite/auto_approve.rs @@ -0,0 +1,86 @@ +use std::borrow::Cow; +use std::path::Path; +use std::process::Stdio; +use std::sync::Arc; + +use anyhow::Result; +use pretty_assertions::assert_eq; +use rmcp::ServiceExt; +use rmcp::model::Tool; +use rmcp::model::object; +use rmcp::transport::ConfigureCommandExt; +use rmcp::transport::TokioChildProcess; +use serde_json::json; +use tokio::process::Command; + +#[tokio::test(flavor = "current_thread")] +async fn auto_approve() -> Result<()> { + let mcp_executable = assert_cmd::Command::cargo_bin("codex-exec-mcp-server")?; + let execve_wrapper = assert_cmd::Command::cargo_bin("codex-execve-wrapper")?; + let bash = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("suite") + .join("bash"); + let transport = + TokioChildProcess::new(Command::new(mcp_executable.get_program()).configure(|cmd| { + cmd.arg("--bash").arg(bash); + cmd.arg("--execve").arg(execve_wrapper.get_program()); + + // Important: pipe stdio so rmcp can speak JSON-RPC over stdin/stdout + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + + // Optional but very helpful while debugging: + cmd.stderr(Stdio::inherit()); + }))?; + + let service = ().serve(transport).await?; + let tools = service.list_tools(Default::default()).await?.tools; + assert_eq!( + vec![Tool { + name: Cow::Borrowed("shell"), + title: None, + description: Some(Cow::Borrowed( + "Runs a shell command and returns its output. You MUST provide the workdir as an absolute path." + )), + input_schema: Arc::new(object(json!( { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "command": { + "description": "The bash string to execute.", + "type": "string", + }, + "login": { + "description": "Launch Bash with -lc instead of -c: defaults to true.", + "nullable": true, + "type": "boolean", + }, + "timeout_ms": { + "description": "The timeout for the command in milliseconds.", + "format": "uint64", + "minimum": 0, + "nullable": true, + "type": "integer", + }, + "workdir": { + "description": "The working directory to execute the command in. Must be an absolute path.", + "type": "string", + }, + }, + "required": [ + "command", + "workdir", + ], + "title": "ExecParams", + "type": "object", + }))), + output_schema: None, + annotations: None, + icons: None, + meta: None + }], + tools + ); + + Ok(()) +} diff --git a/codex-rs/exec-server/tests/suite/bash b/codex-rs/exec-server/tests/suite/bash new file mode 100755 index 0000000000..5c9ffba09e --- /dev/null +++ b/codex-rs/exec-server/tests/suite/bash @@ -0,0 +1,25 @@ +#!/usr/bin/env dotslash + +{ + "name": "codex-bash", + "platforms": { + "macos-aarch64": { + "size": 37003612, + "hash": "blake3", + "digest": "d9cd5928c993b65c340507931c61c02bd6e9179933f8bf26a548482bb5fa53bb", + "format": "tar.gz", + "path": "package/vendor/aarch64-apple-darwin/bash/macos-15/bash", + "providers": [ + { + "url": "https://github.com/openai/codex/releases/download/rust-v0.65.0/codex-shell-tool-mcp-npm-0.65.0.tgz" + }, + { + "type": "github-release", + "repo": "openai/codex", + "tag": "rust-v0.65.0", + "name": "codex-shell-tool-mcp-npm-0.65.0.tgz" + } + ] + } + } +} diff --git a/codex-rs/exec-server/tests/suite/mod.rs b/codex-rs/exec-server/tests/suite/mod.rs new file mode 100644 index 0000000000..1008062c02 --- /dev/null +++ b/codex-rs/exec-server/tests/suite/mod.rs @@ -0,0 +1,3 @@ +// TODO(mbolin): Open this up to more OS's once the Bash DotSlash file includes other platforms. +#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +mod auto_approve;