From ccba737d26ea58532f33b4bbf14af87944c5dc1a Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Wed, 7 Jan 2026 20:56:48 -0800 Subject: [PATCH 1/3] add ability to disable input temporarily in the TUI. (#8876) We will disable input while the elevated sandbox setup is running. --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 75 ++++++++++++++++++- codex-rs/tui/src/bottom_pane/mod.rs | 10 +++ .../tui2/src/bottom_pane/chat_composer.rs | 75 ++++++++++++++++++- codex-rs/tui2/src/bottom_pane/mod.rs | 10 +++ 4 files changed, 166 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 6d47739c1b..f0ac9ca47f 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -110,6 +110,9 @@ pub(crate) struct ChatComposer { attached_images: Vec, placeholder_text: String, is_task_running: bool, + /// When false, the composer is temporarily read-only (e.g. during sandbox setup). + input_enabled: bool, + input_disabled_placeholder: Option, // Non-bracketed paste burst tracker. paste_burst: PasteBurst, // When true, disables paste-burst logic and inserts characters immediately. @@ -160,6 +163,8 @@ impl ChatComposer { attached_images: Vec::new(), placeholder_text, is_task_running: false, + input_enabled: true, + input_disabled_placeholder: None, paste_burst: PasteBurst::default(), disable_paste_burst: false, custom_prompts: Vec::new(), @@ -488,6 +493,10 @@ impl ChatComposer { /// Handle a key event coming from the main UI. pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) { + if !self.input_enabled { + return (InputResult::None, false); + } + let result = match &mut self.active_popup { ActivePopup::Command(_) => self.handle_key_event_with_slash_popup(key_event), ActivePopup::File(_) => self.handle_key_event_with_file_popup(key_event), @@ -1877,6 +1886,17 @@ impl ChatComposer { self.has_focus = has_focus; } + #[allow(dead_code)] + pub(crate) fn set_input_enabled(&mut self, enabled: bool, placeholder: Option) { + self.input_enabled = enabled; + self.input_disabled_placeholder = if enabled { None } else { placeholder }; + + // Avoid leaving interactive popups open while input is blocked. + if !enabled && !matches!(self.active_popup, ActivePopup::None) { + self.active_popup = ActivePopup::None; + } + } + pub fn set_task_running(&mut self, running: bool) { self.is_task_running = running; } @@ -1902,6 +1922,10 @@ impl ChatComposer { impl Renderable for ChatComposer { fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { + if !self.input_enabled { + return None; + } + let [_, textarea_rect, _] = self.layout_areas(area); let state = *self.textarea_state.borrow(); self.textarea.cursor_pos_with_state(textarea_rect, state) @@ -1980,10 +2004,15 @@ impl Renderable for ChatComposer { let style = user_message_style(); Block::default().style(style).render_ref(composer_rect, buf); if !textarea_rect.is_empty() { + let prompt = if self.input_enabled { + "›".bold() + } else { + "›".dim() + }; buf.set_span( textarea_rect.x - LIVE_PREFIX_COLS, textarea_rect.y, - &"›".bold(), + &prompt, textarea_rect.width, ); } @@ -1991,7 +2020,15 @@ impl Renderable for ChatComposer { let mut state = self.textarea_state.borrow_mut(); StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); if self.textarea.text().is_empty() { - let placeholder = Span::from(self.placeholder_text.as_str()).dim(); + let text = if self.input_enabled { + self.placeholder_text.as_str().to_string() + } else { + self.input_disabled_placeholder + .as_deref() + .unwrap_or("Input disabled.") + .to_string() + }; + let placeholder = Span::from(text).dim().italic(); Line::from(vec![placeholder]).render_ref(textarea_rect.inner(Margin::new(0, 0)), buf); } } @@ -4389,4 +4426,38 @@ mod tests { ); assert_eq!(composer.attached_images.len(), 1); } + #[test] + fn input_disabled_ignores_keypresses_and_hides_cursor() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + composer.set_text_content("hello".to_string()); + composer.set_input_enabled(false, Some("Input disabled for test.".to_string())); + + let (result, needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)); + + assert_eq!(result, InputResult::None); + assert!(!needs_redraw); + assert_eq!(composer.current_text(), "hello"); + + let area = Rect { + x: 0, + y: 0, + width: 40, + height: 5, + }; + assert_eq!(composer.cursor_pos(area), None); + } } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 329d8ec805..fe626537ac 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -264,6 +264,16 @@ impl BottomPane { self.request_redraw(); } + #[allow(dead_code)] + pub(crate) fn set_composer_input_enabled( + &mut self, + enabled: bool, + placeholder: Option, + ) { + self.composer.set_input_enabled(enabled, placeholder); + self.request_redraw(); + } + pub(crate) fn clear_composer_for_ctrl_c(&mut self) { self.composer.clear_for_ctrl_c(); self.request_redraw(); diff --git a/codex-rs/tui2/src/bottom_pane/chat_composer.rs b/codex-rs/tui2/src/bottom_pane/chat_composer.rs index e136b81971..daa861f58b 100644 --- a/codex-rs/tui2/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui2/src/bottom_pane/chat_composer.rs @@ -113,6 +113,9 @@ pub(crate) struct ChatComposer { attached_images: Vec, placeholder_text: String, is_task_running: bool, + /// When false, the composer is temporarily read-only (e.g. during sandbox setup). + input_enabled: bool, + input_disabled_placeholder: Option, // Non-bracketed paste burst tracker. paste_burst: PasteBurst, // When true, disables paste-burst logic and inserts characters immediately. @@ -168,6 +171,8 @@ impl ChatComposer { attached_images: Vec::new(), placeholder_text, is_task_running: false, + input_enabled: true, + input_disabled_placeholder: None, paste_burst: PasteBurst::default(), disable_paste_burst: false, custom_prompts: Vec::new(), @@ -405,6 +410,10 @@ impl ChatComposer { /// Handle a key event coming from the main UI. pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) { + if !self.input_enabled { + return (InputResult::None, false); + } + let result = match &mut self.active_popup { ActivePopup::Command(_) => self.handle_key_event_with_slash_popup(key_event), ActivePopup::File(_) => self.handle_key_event_with_file_popup(key_event), @@ -1819,6 +1828,17 @@ impl ChatComposer { self.has_focus = has_focus; } + #[allow(dead_code)] + pub(crate) fn set_input_enabled(&mut self, enabled: bool, placeholder: Option) { + self.input_enabled = enabled; + self.input_disabled_placeholder = if enabled { None } else { placeholder }; + + // Avoid leaving interactive popups open while input is blocked. + if !enabled && !matches!(self.active_popup, ActivePopup::None) { + self.active_popup = ActivePopup::None; + } + } + pub fn set_task_running(&mut self, running: bool) { self.is_task_running = running; } @@ -1844,6 +1864,10 @@ impl ChatComposer { impl Renderable for ChatComposer { fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { + if !self.input_enabled { + return None; + } + let [_, textarea_rect, _] = self.layout_areas(area); let state = *self.textarea_state.borrow(); self.textarea.cursor_pos_with_state(textarea_rect, state) @@ -1922,10 +1946,15 @@ impl Renderable for ChatComposer { let style = user_message_style(); Block::default().style(style).render_ref(composer_rect, buf); if !textarea_rect.is_empty() { + let prompt = if self.input_enabled { + "›".bold() + } else { + "›".dim() + }; buf.set_span( textarea_rect.x - LIVE_PREFIX_COLS, textarea_rect.y, - &"›".bold(), + &prompt, textarea_rect.width, ); } @@ -1933,7 +1962,15 @@ impl Renderable for ChatComposer { let mut state = self.textarea_state.borrow_mut(); StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); if self.textarea.text().is_empty() { - let placeholder = Span::from(self.placeholder_text.as_str()).dim(); + let text = if self.input_enabled { + self.placeholder_text.as_str().to_string() + } else { + self.input_disabled_placeholder + .as_deref() + .unwrap_or("Input disabled.") + .to_string() + }; + let placeholder = Span::from(text).dim().italic(); Line::from(vec![placeholder]).render_ref(textarea_rect.inner(Margin::new(0, 0)), buf); } } @@ -4108,4 +4145,38 @@ mod tests { "'/zzz' should not activate slash popup because it is not a prefix of any built-in command" ); } + #[test] + fn input_disabled_ignores_keypresses_and_hides_cursor() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + composer.set_text_content("hello".to_string()); + composer.set_input_enabled(false, Some("Input disabled for test.".to_string())); + + let (result, needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)); + + assert_eq!(result, InputResult::None); + assert!(!needs_redraw); + assert_eq!(composer.current_text(), "hello"); + + let area = Rect { + x: 0, + y: 0, + width: 40, + height: 5, + }; + assert_eq!(composer.cursor_pos(area), None); + } } diff --git a/codex-rs/tui2/src/bottom_pane/mod.rs b/codex-rs/tui2/src/bottom_pane/mod.rs index 2ebd0715e7..4b6caf0d1a 100644 --- a/codex-rs/tui2/src/bottom_pane/mod.rs +++ b/codex-rs/tui2/src/bottom_pane/mod.rs @@ -256,6 +256,16 @@ impl BottomPane { self.request_redraw(); } + #[allow(dead_code)] + pub(crate) fn set_composer_input_enabled( + &mut self, + enabled: bool, + placeholder: Option, + ) { + self.composer.set_input_enabled(enabled, placeholder); + self.request_redraw(); + } + pub(crate) fn clear_composer_for_ctrl_c(&mut self) { self.composer.clear_for_ctrl_c(); self.request_redraw(); From 35fd69a9f0d018a652009a39e24f235769f40101 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 7 Jan 2026 23:03:43 -0800 Subject: [PATCH 2/3] fix: make the find_resource! macro responsible for the absolutize() call (#8884) https://github.com/openai/codex/pull/8879 introduced the `find_resource!` macro, but now that I am about to use it in more places, I realize that it should take care of this normalization case for callers. Note the `use $crate::path_absolutize::Absolutize;` line is there so that users of `find_resource!` do not have to explicitly include `path-absolutize` to their own `Cargo.toml`. --- codex-rs/Cargo.lock | 2 +- codex-rs/exec-server/tests/common/Cargo.toml | 1 - codex-rs/exec-server/tests/common/lib.rs | 11 ++----- codex-rs/utils/cargo-bin/Cargo.toml | 1 + codex-rs/utils/cargo-bin/src/lib.rs | 30 +++++++++++++++----- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index add99854fb..56325a61e7 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1881,6 +1881,7 @@ name = "codex-utils-cargo-bin" version = "0.0.0" dependencies = [ "assert_cmd", + "path-absolutize", "thiserror 2.0.17", ] @@ -2845,7 +2846,6 @@ dependencies = [ "anyhow", "codex-core", "codex-utils-cargo-bin", - "path-absolutize", "rmcp", "serde_json", "tokio", diff --git a/codex-rs/exec-server/tests/common/Cargo.toml b/codex-rs/exec-server/tests/common/Cargo.toml index 4846db52f7..6444b61f97 100644 --- a/codex-rs/exec-server/tests/common/Cargo.toml +++ b/codex-rs/exec-server/tests/common/Cargo.toml @@ -11,7 +11,6 @@ path = "lib.rs" anyhow = { workspace = true } codex-core = { workspace = true } codex-utils-cargo-bin = { workspace = true } -path-absolutize = { workspace = true } rmcp = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } diff --git a/codex-rs/exec-server/tests/common/lib.rs b/codex-rs/exec-server/tests/common/lib.rs index b587868d80..562d3504f6 100644 --- a/codex-rs/exec-server/tests/common/lib.rs +++ b/codex-rs/exec-server/tests/common/lib.rs @@ -2,7 +2,6 @@ use codex_core::MCP_SANDBOX_STATE_METHOD; use codex_core::SandboxState; use codex_core::protocol::SandboxPolicy; use codex_utils_cargo_bin::find_resource; -use path_absolutize::Absolutize; use rmcp::ClientHandler; use rmcp::ErrorData as McpError; use rmcp::RoleClient; @@ -38,14 +37,8 @@ where let execve_wrapper = codex_utils_cargo_bin::cargo_bin("codex-execve-wrapper")?; // `bash` is a test resource rather than a binary target, so we must use - // `find_resource!` to locate it instead of `cargo_bin`. - // - // Note we also have to normalize (but not canonicalize!) the path for - // _Bazel_ because the original value ends with - // `codex-rs/exec-server/tests/common/../suite/bash`, but the `tests/common` - // folder will not exist at runtime under Bazel. As such, we have to - // normalize it before passing it to `dotslash fetch`. - let bash = find_resource!("../suite/bash")?.absolutize()?.to_path_buf(); + // `find_resource!` to locate it instead of `cargo_bin()`. + let bash = find_resource!("../suite/bash")?; // Need to ensure the artifact associated with the bash DotSlash file is // available before it is run in a read-only sandbox. diff --git a/codex-rs/utils/cargo-bin/Cargo.toml b/codex-rs/utils/cargo-bin/Cargo.toml index d8e6877e6e..fe3a410547 100644 --- a/codex-rs/utils/cargo-bin/Cargo.toml +++ b/codex-rs/utils/cargo-bin/Cargo.toml @@ -9,4 +9,5 @@ workspace = true [dependencies] assert_cmd = { workspace = true } +path-absolutize = { workspace = true } thiserror = { workspace = true } diff --git a/codex-rs/utils/cargo-bin/src/lib.rs b/codex-rs/utils/cargo-bin/src/lib.rs index 2858ad6bca..40fa40c62f 100644 --- a/codex-rs/utils/cargo-bin/src/lib.rs +++ b/codex-rs/utils/cargo-bin/src/lib.rs @@ -1,6 +1,8 @@ use std::ffi::OsString; use std::path::PathBuf; +pub use path_absolutize; + #[derive(Debug, thiserror::Error)] pub enum CargoBinError { #[error("failed to read current exe")] @@ -91,19 +93,33 @@ macro_rules! find_resource { // included in the compiled binary (even if it is built with Cargo), but // we only check it at runtime if `RUNFILES_DIR` is set. let resource = std::path::Path::new(&$resource); - let manifest_dir = match std::env::var("RUNFILES_DIR") { + match std::env::var("RUNFILES_DIR") { Ok(bazel_runtime_files) => match option_env!("BAZEL_PACKAGE") { - Some(bazel_package) => Ok(std::path::PathBuf::from(bazel_runtime_files) - .join("_main") - .join(bazel_package)), + Some(bazel_package) => { + use $crate::path_absolutize::Absolutize; + + let manifest_dir = std::path::PathBuf::from(bazel_runtime_files) + .join("_main") + .join(bazel_package) + .join(resource); + // Note we also have to normalize (but not canonicalize!) + // the path for _Bazel_ because the original value ends with + // `codex-rs/exec-server/tests/common/../suite/bash`, but + // the `tests/common` folder will not exist at runtime under + // Bazel. As such, we have to normalize it before passing it + // to `dotslash fetch`. + manifest_dir.absolutize().map(|p| p.to_path_buf()) + } None => Err(std::io::Error::new( std::io::ErrorKind::NotFound, "BAZEL_PACKAGE not set in Bazel build", )), }, - Err(_) => Ok(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))), - }; - manifest_dir.map(|dir| dir.join(resource)) + Err(_) => { + let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + Ok(manifest_dir.join(resource)) + } + } }}; } From ccedbba6968a7f6ece0c7e0812f6f3ba5df743e1 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 8 Jan 2026 00:00:10 -0800 Subject: [PATCH 3/3] fix: leverage codex_utils_cargo_bin() in codex-rs/core/tests/suite --- codex-rs/core/Cargo.toml | 1 - codex-rs/core/tests/common/lib.rs | 5 +++ codex-rs/core/tests/suite/rmcp_client.rs | 53 ++++-------------------- codex-rs/core/tests/suite/truncation.rs | 26 ++---------- 4 files changed, 18 insertions(+), 67 deletions(-) diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 5060092290..b0b98f04b0 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -126,7 +126,6 @@ codex-core = { path = ".", features = ["deterministic_process_ids"] } codex-utils-cargo-bin = { workspace = true } core_test_support = { workspace = true } ctor = { workspace = true } -escargot = { workspace = true } image = { workspace = true, features = ["jpeg", "png"] } maplit = { workspace = true } predicates = { workspace = true } diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 45e8b0b46f..7c5e5ca173 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -1,5 +1,6 @@ #![expect(clippy::expect_used)] +use codex_utils_cargo_bin::CargoBinError; use tempfile::TempDir; use codex_core::CodexThread; @@ -235,6 +236,10 @@ pub fn format_with_current_shell_display_non_login(command: &str) -> String { .expect("serialize current shell command without login") } +pub fn stdio_server_bin() -> Result { + codex_utils_cargo_bin::cargo_bin("test_stdio_server").map(|p| p.to_string_lossy().to_string()) +} + pub mod fs_wait { use anyhow::Result; use anyhow::anyhow; diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 617b3b8a21..9274369967 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -19,12 +19,13 @@ use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::user_input::UserInput; +use codex_utils_cargo_bin::cargo_bin; use core_test_support::responses; use core_test_support::responses::mount_sse_once; use core_test_support::skip_if_no_network; +use core_test_support::stdio_server_bin; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; -use escargot::CargoBuild; use mcp_types::ContentBlock; use serde_json::Value; use serde_json::json; @@ -68,13 +69,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { .await; let expected_env_value = "propagated-env"; - let rmcp_test_server_bin = CargoBuild::new() - .package("codex-rmcp-client") - .bin("test_stdio_server") - .run()? - .path() - .to_string_lossy() - .into_owned(); + let rmcp_test_server_bin = stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { @@ -82,7 +77,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { server_name.to_string(), McpServerConfig { transport: McpServerTransportConfig::Stdio { - command: rmcp_test_server_bin.clone(), + command: rmcp_test_server_bin, args: Vec::new(), env: Some(HashMap::from([( "MCP_TEST_VALUE".to_string(), @@ -205,13 +200,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { .await; // Build the stdio rmcp server and pass the image as data URL so it can construct ImageContent. - let rmcp_test_server_bin = CargoBuild::new() - .package("codex-rmcp-client") - .bin("test_stdio_server") - .run()? - .path() - .to_string_lossy() - .into_owned(); + let rmcp_test_server_bin = stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { @@ -399,13 +388,7 @@ async fn stdio_image_completions_round_trip() -> anyhow::Result<()> { .mount(&server) .await; - let rmcp_test_server_bin = CargoBuild::new() - .package("codex-rmcp-client") - .bin("test_stdio_server") - .run()? - .path() - .to_string_lossy() - .into_owned(); + let rmcp_test_server_bin = stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { @@ -546,13 +529,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { let expected_env_value = "propagated-env-from-whitelist"; let _guard = EnvVarGuard::set("MCP_TEST_VALUE", OsStr::new(expected_env_value)); - let rmcp_test_server_bin = CargoBuild::new() - .package("codex-rmcp-client") - .bin("test_stdio_server") - .run()? - .path() - .to_string_lossy() - .into_owned(); + let rmcp_test_server_bin = stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { @@ -680,13 +657,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { .await; let expected_env_value = "propagated-env-http"; - let rmcp_http_server_bin = CargoBuild::new() - .package("codex-rmcp-client") - .bin("test_streamable_http_server") - .run()? - .path() - .to_string_lossy() - .into_owned(); + let rmcp_http_server_bin = cargo_bin("test_streamable_http_server")?; let listener = TcpListener::bind("127.0.0.1:0")?; let port = listener.local_addr()?.port(); @@ -848,13 +819,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> { let expected_token = "initial-access-token"; let client_id = "test-client-id"; let refresh_token = "initial-refresh-token"; - let rmcp_http_server_bin = CargoBuild::new() - .package("codex-rmcp-client") - .bin("test_streamable_http_server") - .run()? - .path() - .to_string_lossy() - .into_owned(); + let rmcp_http_server_bin = cargo_bin("test_streamable_http_server")?; let listener = TcpListener::bind("127.0.0.1:0")?; let port = listener.local_addr()?.port(); diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index 4b916e51b4..0176f8cde3 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -22,9 +22,9 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; +use core_test_support::stdio_server_bin; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; -use escargot::CargoBuild; use serde_json::Value; use serde_json::json; use std::collections::HashMap; @@ -411,13 +411,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> .await; // Compile the rmcp stdio test server and configure it. - let rmcp_test_server_bin = CargoBuild::new() - .package("codex-rmcp-client") - .bin("test_stdio_server") - .run()? - .path() - .to_string_lossy() - .into_owned(); + let rmcp_test_server_bin = stdio_server_bin()?; let mut builder = test_codex().with_config(move |config| { config.mcp_servers.insert( @@ -497,13 +491,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { .await; // Build the stdio rmcp server and pass a tiny PNG via data URL so it can construct ImageContent. - let rmcp_test_server_bin = CargoBuild::new() - .package("codex-rmcp-client") - .bin("test_stdio_server") - .run()? - .path() - .to_string_lossy() - .into_owned(); + let rmcp_test_server_bin = stdio_server_bin()?; // 1x1 PNG data URL let openai_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMB/ee9bQAAAABJRU5ErkJggg=="; @@ -762,13 +750,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> { ) .await; - let rmcp_test_server_bin = CargoBuild::new() - .package("codex-rmcp-client") - .bin("test_stdio_server") - .run()? - .path() - .to_string_lossy() - .into_owned(); + let rmcp_test_server_bin = stdio_server_bin()?; let mut builder = test_codex().with_config(move |config| { config.tool_output_token_limit = Some(50_000);