From bfd4e2112cbf6c7ca3c056aa99a8ec0297a95d63 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Tue, 10 Feb 2026 18:50:11 -0800 Subject: [PATCH 1/3] Disable very flaky tests (#11394) Collected from last 20 builds of main in https://github.com/openai/codex/commits/main/. --- codex-rs/app-server/tests/suite/v2/review.rs | 1 + codex-rs/core/tests/suite/pending_input.rs | 1 + codex-rs/state/src/runtime.rs | 1 + codex-rs/tui/tests/suite/no_panic_on_startup.rs | 1 + 4 files changed, 4 insertions(+) diff --git a/codex-rs/app-server/tests/suite/v2/review.rs b/codex-rs/app-server/tests/suite/v2/review.rs index 1250c055dd..441ad2ce19 100644 --- a/codex-rs/app-server/tests/suite/v2/review.rs +++ b/codex-rs/app-server/tests/suite/v2/review.rs @@ -138,6 +138,7 @@ async fn review_start_runs_review_turn_and_emits_code_review_item() -> Result<() } #[tokio::test] +#[ignore = "TODO(owenlin0): flaky"] async fn review_start_exec_approval_item_id_matches_command_execution_item() -> Result<()> { let responses = vec![ create_shell_command_sse_response( diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index ad662d901b..c89bcb9a90 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -45,6 +45,7 @@ fn message_input_texts(body: &Value, role: &str) -> Vec { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "TODO(aibrahim): flaky"] async fn injected_user_input_triggers_follow_up_request_with_deltas() { let (gate_completed_tx, gate_completed_rx) = oneshot::channel(); diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 541e02ad38..dcb472022b 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -1272,6 +1272,7 @@ WHERE id = 1 } #[tokio::test] + #[ignore = "TODO(jif): flaky"] async fn stage1_concurrent_claims_respect_running_cap() { let codex_home = unique_temp_dir(); let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) diff --git a/codex-rs/tui/tests/suite/no_panic_on_startup.rs b/codex-rs/tui/tests/suite/no_panic_on_startup.rs index eade57d08e..3984e771df 100644 --- a/codex-rs/tui/tests/suite/no_panic_on_startup.rs +++ b/codex-rs/tui/tests/suite/no_panic_on_startup.rs @@ -6,6 +6,7 @@ use tokio::time::timeout; /// Regression test for https://github.com/openai/codex/issues/8803. #[tokio::test] +#[ignore = "TODO(mbolin): flaky"] async fn malformed_rules_should_not_panic() -> anyhow::Result<()> { // run_codex_cli() does not work on Windows due to PTY limitations. if cfg!(windows) { From c68999ee6dec3a3a9d5cf58352a432a540feb938 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Tue, 10 Feb 2026 18:50:48 -0800 Subject: [PATCH 2/3] Prefer websocket transport when model opts in (#11386) Summary - add a `prefer_websockets` field to `ModelInfo`, defaulting to `false` in all fixtures and constructors - wire the new flag into websocket selection so models that opt in always use websocket transport even when the feature gate is off Testing - Not run (not requested) --- .../app-server/tests/common/models_cache.rs | 1 + .../codex-api/tests/models_integration.rs | 1 + codex-rs/core/src/client.rs | 23 ++++++--- codex-rs/core/src/codex.rs | 13 +++-- .../core/src/models_manager/model_info.rs | 1 + codex-rs/core/src/tasks/regular.rs | 4 +- .../core/tests/suite/client_websockets.rs | 51 ++++++++++++++++--- codex-rs/core/tests/suite/model_switching.rs | 1 + codex-rs/core/tests/suite/models_cache_ttl.rs | 1 + codex-rs/core/tests/suite/personality.rs | 3 ++ codex-rs/core/tests/suite/remote_models.rs | 3 ++ codex-rs/core/tests/suite/rmcp_client.rs | 1 + codex-rs/core/tests/suite/view_image.rs | 1 + codex-rs/protocol/src/openai_models.rs | 4 ++ 14 files changed, 89 insertions(+), 19 deletions(-) diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 14b4e8d458..00359b28d8 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -40,6 +40,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), input_modalities: default_input_modalities(), + prefer_websockets: false, } } diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 8442133b4d..b33f8b308b 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -88,6 +88,7 @@ async fn models_client_hits_models_endpoint() { effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), input_modalities: default_input_modalities(), + prefer_websockets: false, }], }; diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index ee4bc6f427..0999c3291d 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -340,8 +340,9 @@ impl ModelClient { /// /// This combines provider capability and feature gating; both must be true for websocket paths /// to be eligible. - fn responses_websocket_enabled(&self) -> bool { - self.state.provider.supports_websockets && self.state.enable_responses_websockets + fn responses_websocket_enabled(&self, model_info: &ModelInfo) -> bool { + self.state.provider.supports_websockets + && (self.state.enable_responses_websockets || model_info.prefer_websockets) } fn responses_websockets_v2_enabled(&self) -> bool { @@ -612,9 +613,11 @@ impl ModelClientSession { pub async fn prewarm_websocket( &mut self, otel_manager: &OtelManager, + model_info: &ModelInfo, turn_metadata_header: Option<&str>, ) -> std::result::Result<(), ApiError> { - if !self.client.responses_websocket_enabled() || self.client.disable_websockets() { + if !self.client.responses_websocket_enabled(model_info) || self.client.disable_websockets() + { return Ok(()); } if self.connection.is_some() { @@ -881,8 +884,8 @@ impl ModelClientSession { let wire_api = self.client.state.provider.wire_api; match wire_api { WireApi::Responses => { - let websocket_enabled = - self.client.responses_websocket_enabled() && !self.client.disable_websockets(); + let websocket_enabled = self.client.responses_websocket_enabled(model_info) + && !self.client.disable_websockets(); if websocket_enabled { match self @@ -898,7 +901,7 @@ impl ModelClientSession { { WebsocketStreamOutcome::Stream(stream) => return Ok(stream), WebsocketStreamOutcome::FallbackToHttp => { - self.try_switch_fallback_transport(otel_manager); + self.try_switch_fallback_transport(otel_manager, model_info); } } } @@ -922,8 +925,12 @@ impl ModelClientSession { /// the HTTP transport. /// /// Returns `true` if this call activated fallback, or `false` if fallback was already active. - pub(crate) fn try_switch_fallback_transport(&mut self, otel_manager: &OtelManager) -> bool { - let websocket_enabled = self.client.responses_websocket_enabled(); + pub(crate) fn try_switch_fallback_transport( + &mut self, + otel_manager: &OtelManager, + model_info: &ModelInfo, + ) -> bool { + let websocket_enabled = self.client.responses_websocket_enabled(model_info); let activated = self.activate_http_fallback(websocket_enabled); if activated { warn!("falling back to HTTP"); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index cd653793a6..7ee094b566 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1128,6 +1128,9 @@ impl Session { ), }; + let prewarm_model_info = models_manager + .get_model_info(session_configuration.collaboration_mode.model(), &config) + .await; let prewarm_cwd = session_configuration.cwd.clone(); let turn_metadata_header = resolve_turn_metadata_header_with_timeout( async move { build_turn_metadata_header(prewarm_cwd.as_path(), None).await }, @@ -1137,6 +1140,7 @@ impl Session { let startup_regular_task = RegularTask::with_startup_prewarm( services.model_client.clone(), services.otel_manager.clone(), + prewarm_model_info, turn_metadata_header, ); state.set_startup_regular_task(startup_regular_task); @@ -4293,7 +4297,8 @@ async fn run_sampling_request( // Use the configured provider-specific stream retry budget. let max_retries = turn_context.provider.stream_max_retries(); if retries >= max_retries - && client_session.try_switch_fallback_transport(&turn_context.otel_manager) + && client_session + .try_switch_fallback_transport(&turn_context.otel_manager, &turn_context.model_info) { sess.send_event( &turn_context, @@ -6261,7 +6266,8 @@ mod tests { session_configuration.provider.clone(), session_configuration.session_source.clone(), config.model_verbosity, - config.features.enabled(Feature::ResponsesWebsockets) + model_info.prefer_websockets + || config.features.enabled(Feature::ResponsesWebsockets) || config.features.enabled(Feature::ResponsesWebsocketsV2), config.features.enabled(Feature::ResponsesWebsocketsV2), config.features.enabled(Feature::EnableRequestCompression), @@ -6396,7 +6402,8 @@ mod tests { session_configuration.provider.clone(), session_configuration.session_source.clone(), config.model_verbosity, - config.features.enabled(Feature::ResponsesWebsockets) + model_info.prefer_websockets + || config.features.enabled(Feature::ResponsesWebsockets) || config.features.enabled(Feature::ResponsesWebsocketsV2), config.features.enabled(Feature::ResponsesWebsocketsV2), config.features.enabled(Feature::EnableRequestCompression), diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs index b062a93a17..6a29cd96f9 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -80,6 +80,7 @@ pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo { effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), input_modalities: default_input_modalities(), + prefer_websockets: false, } } diff --git a/codex-rs/core/src/tasks/regular.rs b/codex-rs/core/src/tasks/regular.rs index 2bc9002b51..8782d7cedb 100644 --- a/codex-rs/core/src/tasks/regular.rs +++ b/codex-rs/core/src/tasks/regular.rs @@ -8,6 +8,7 @@ use crate::codex::run_turn; use crate::state::TaskKind; use async_trait::async_trait; use codex_otel::OtelManager; +use codex_protocol::openai_models::ModelInfo; use codex_protocol::user_input::UserInput; use futures::future::BoxFuture; use tokio::task::JoinHandle; @@ -37,13 +38,14 @@ impl RegularTask { pub(crate) fn with_startup_prewarm( model_client: ModelClient, otel_manager: OtelManager, + model_info: ModelInfo, turn_metadata_header: BoxFuture<'static, Option>, ) -> Self { let prewarmed_session_task = tokio::spawn(async move { let mut client_session = model_client.new_session(); let turn_metadata_header = turn_metadata_header.await; match client_session - .prewarm_websocket(&otel_manager, turn_metadata_header.as_deref()) + .prewarm_websocket(&otel_manager, &model_info, turn_metadata_header.as_deref()) .await { Ok(()) => Some(client_session), diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index d1ebc63777..c4f63f8440 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -105,7 +105,7 @@ async fn responses_websocket_preconnect_reuses_connection() { let harness = websocket_harness(&server).await; let mut client_session = harness.client.new_session(); client_session - .prewarm_websocket(&harness.otel_manager, None) + .prewarm_websocket(&harness.otel_manager, &harness.model_info, None) .await .expect("websocket prewarm failed"); let prompt = prompt_with_input(vec![message_item("hello")]); @@ -130,7 +130,7 @@ async fn responses_websocket_preconnect_is_reused_even_with_header_changes() { let harness = websocket_harness(&server).await; let mut client_session = harness.client.new_session(); client_session - .prewarm_websocket(&harness.otel_manager, None) + .prewarm_websocket(&harness.otel_manager, &harness.model_info, None) .await .expect("websocket prewarm failed"); let prompt = prompt_with_input(vec![message_item("hello")]); @@ -158,6 +158,36 @@ async fn responses_websocket_preconnect_is_reused_even_with_header_changes() { server.shutdown().await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn responses_websocket_prewarm_uses_model_preference_when_feature_disabled() { + skip_if_no_network!(); + + let server = start_websocket_server(vec![vec![vec![ + ev_response_created("resp-1"), + ev_completed("resp-1"), + ]]]) + .await; + + let harness = websocket_harness_with_options(&server, false, false, false, true).await; + let mut client_session = harness.client.new_session(); + client_session + .prewarm_websocket(&harness.otel_manager, &harness.model_info, None) + .await + .expect("websocket prewarm failed"); + + // Prewarm should only perform the handshake, not send response.create. + assert_eq!(server.handshakes().len(), 1); + assert_eq!(server.single_connection().len(), 0); + + let prompt = prompt_with_input(vec![message_item("hello")]); + stream_until_complete(&mut client_session, &harness, &prompt).await; + + assert_eq!(server.handshakes().len(), 1); + assert_eq!(server.single_connection().len(), 1); + + server.shutdown().await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[traced_test] async fn responses_websocket_emits_websocket_telemetry_events() { @@ -887,26 +917,32 @@ async fn websocket_harness_with_runtime_metrics( server: &WebSocketTestServer, runtime_metrics_enabled: bool, ) -> WebsocketTestHarness { - websocket_harness_with_options(server, runtime_metrics_enabled, false).await + websocket_harness_with_options(server, runtime_metrics_enabled, true, false, false).await } async fn websocket_harness_with_v2( server: &WebSocketTestServer, websocket_v2_enabled: bool, ) -> WebsocketTestHarness { - websocket_harness_with_options(server, false, websocket_v2_enabled).await + websocket_harness_with_options(server, false, true, websocket_v2_enabled, false).await } async fn websocket_harness_with_options( server: &WebSocketTestServer, runtime_metrics_enabled: bool, + websocket_enabled: bool, websocket_v2_enabled: bool, + prefer_websockets: bool, ) -> WebsocketTestHarness { let provider = websocket_provider(server); let codex_home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&codex_home).await; config.model = Some(MODEL.to_string()); - config.features.enable(Feature::ResponsesWebsockets); + if websocket_enabled { + config.features.enable(Feature::ResponsesWebsockets); + } else { + config.features.disable(Feature::ResponsesWebsockets); + } if runtime_metrics_enabled { config.features.enable(Feature::RuntimeMetrics); } @@ -914,7 +950,8 @@ async fn websocket_harness_with_options( config.features.enable(Feature::ResponsesWebsocketsV2); } let config = Arc::new(config); - let model_info = ModelsManager::construct_model_info_offline(MODEL, &config); + let mut model_info = ModelsManager::construct_model_info_offline(MODEL, &config); + model_info.prefer_websockets = prefer_websockets; let conversation_id = ThreadId::new(); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let exporter = InMemoryMetricExporter::default(); @@ -944,7 +981,7 @@ async fn websocket_harness_with_options( provider.clone(), SessionSource::Exec, config.model_verbosity, - true, + websocket_enabled, websocket_v2_enabled, false, runtime_metrics_enabled, diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index b76dcb0467..fddd3d282d 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -225,6 +225,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< visibility: ModelVisibility::List, supported_in_api: true, input_modalities: default_input_modalities(), + prefer_websockets: false, priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 11cdd369e9..59c7dde7d7 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -351,5 +351,6 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), input_modalities: default_input_modalities(), + prefer_websockets: false, } } diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 4fdedd0798..5673e317f6 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -613,6 +613,7 @@ async fn ignores_remote_personality_if_remote_models_disabled() -> anyhow::Resul effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), input_modalities: default_input_modalities(), + prefer_websockets: false, }; let _models_mock = mount_models_once( @@ -729,6 +730,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), input_modalities: default_input_modalities(), + prefer_websockets: false, }; let _models_mock = mount_models_once( @@ -840,6 +842,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), input_modalities: default_input_modalities(), + prefer_websockets: false, }; let _models_mock = mount_models_once( diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index d0a0ca3860..0af662f07b 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -141,6 +141,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { visibility: ModelVisibility::List, supported_in_api: true, input_modalities: default_input_modalities(), + prefer_websockets: false, priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), @@ -379,6 +380,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { visibility: ModelVisibility::List, supported_in_api: true, input_modalities: default_input_modalities(), + prefer_websockets: false, priority: 1, upgrade: None, base_instructions: remote_base.to_string(), @@ -862,6 +864,7 @@ fn test_remote_model_with_policy( visibility, supported_in_api: true, input_modalities: default_input_modalities(), + prefer_websockets: false, priority, upgrade: None, base_instructions: "base instructions".to_string(), diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 82d4d13a6e..a1bf72b103 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -409,6 +409,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), input_modalities: vec![InputModality::Text], + prefer_websockets: false, }], }, ) diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index cabee944db..0e30d682b8 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -560,6 +560,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an visibility: ModelVisibility::List, supported_in_api: true, input_modalities: vec![InputModality::Text], + prefer_websockets: false, priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 36298e9ca5..f8e61fd57a 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -249,6 +249,9 @@ pub struct ModelInfo { /// Input modalities accepted by the backend for this model. #[serde(default = "default_input_modalities")] pub input_modalities: Vec, + /// When true, this model should use websocket transport even when websocket features are off. + #[serde(default)] + pub prefer_websockets: bool, } impl ModelInfo { @@ -506,6 +509,7 @@ mod tests { effective_context_window_percent: 95, experimental_supported_tools: vec![], input_modalities: default_input_modalities(), + prefer_websockets: false, } } From 6311cd8b59cdf06d2f026b0d64ff07f399802f26 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 10 Feb 2026 18:51:47 -0800 Subject: [PATCH 3/3] Remove `deterministic_process_ids` feature to avoid duplicate `codex-core` builds ## Why `codex-core` enabled `deterministic_process_ids` through a self dev-dependency. That forced a second feature-resolved build of the same crate, which increased compile time and test latency. ## What Changed - Removed the `deterministic_process_ids` feature from `codex-rs/core/Cargo.toml`. - Removed the self dev-dependency on `codex-core` that enabled that feature. - Removed the Bazel `deterministic_process_ids` crate feature for `codex-core`. - Added a test-only `AtomicBool` override in unified exec process-id allocation. - Added a test-support setter for that override and re-exported it from `codex-core`. - Enabled deterministic process IDs in integration tests via `core_test_support` ctor. ## Behavior - Production behavior remains random process IDs. - Unit tests remain deterministic via `cfg(test)`. - Integration tests remain deterministic via explicit test-support initialization. ## Validation - `just fmt` - `cargo test -p codex-core unified_exec::` - `cargo test -p codex-core --test all unified_exec -- --test-threads=1` - `cargo tree -p codex-core -e features` (verified the removed feature path) --- codex-rs/Cargo.lock | 2 +- codex-rs/core/BUILD.bazel | 2 +- codex-rs/core/Cargo.toml | 4 --- codex-rs/core/src/lib.rs | 2 ++ codex-rs/core/src/unified_exec/mod.rs | 5 +++ .../core/src/unified_exec/process_manager.rs | 35 ++++++++++++++++--- codex-rs/core/tests/common/Cargo.toml | 1 + codex-rs/core/tests/common/lib.rs | 6 ++++ 8 files changed, 47 insertions(+), 10 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 2c6a8904dc..4e2877a2c8 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1660,7 +1660,6 @@ dependencies = [ "codex-arg0", "codex-async-utils", "codex-client", - "codex-core", "codex-execpolicy", "codex-file-search", "codex-git", @@ -2639,6 +2638,7 @@ dependencies = [ "codex-protocol", "codex-utils-absolute-path", "codex-utils-cargo-bin", + "ctor 0.6.3", "futures", "notify", "pretty_assertions", diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index f1649b36c7..6e5ce680ae 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -6,7 +6,7 @@ codex_rust_crate( # TODO(mbolin): Eliminate the use of features in the version of the # rust_library() that is used by rust_binary() rules for release artifacts # such as the Codex CLI. - crate_features = ["deterministic_process_ids", "test-support"], + crate_features = ["test-support"], compile_data = glob( include = ["**"], exclude = [ diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index e84f617256..26e7f17cc8 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -114,7 +114,6 @@ wildmatch = { workspace = true } zip = { workspace = true } [features] -deterministic_process_ids = [] test-support = [] @@ -150,9 +149,6 @@ keyring = { workspace = true, features = ["sync-secret-service"] } assert_cmd = { workspace = true } assert_matches = { workspace = true } codex-arg0 = { workspace = true } -codex-core = { path = ".", default-features = false, features = [ - "deterministic_process_ids", -] } codex-otel = { workspace = true, features = [ "disable-default-metrics-exporter", ] } diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 4479e24c3a..85095bc5f5 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -145,6 +145,8 @@ pub use file_watcher::FileWatcherEvent; pub use safety::get_platform_sandbox; pub use tools::spec::parse_tool_input_schema; pub use turn_metadata::build_turn_metadata_header; +#[cfg(any(test, feature = "test-support"))] +pub use unified_exec::set_deterministic_process_ids_for_tests; // Re-export the protocol types from the standalone `codex-protocol` crate so existing // `codex_core::protocol::...` references continue to work across the workspace. pub use codex_protocol::protocol; diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index c1c7b87078..bbc7790c63 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -42,6 +42,11 @@ mod head_tail_buffer; mod process; mod process_manager; +#[cfg(any(test, feature = "test-support"))] +pub fn set_deterministic_process_ids_for_tests(enabled: bool) { + process_manager::set_deterministic_process_ids_for_tests(enabled); +} + pub(crate) use errors::UnifiedExecError; pub(crate) use process::UnifiedExecProcess; diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 8576bf77c0..6740850092 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -5,6 +5,7 @@ use std::collections::HashSet; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; use tokio::sync::Notify; use tokio::sync::mpsc; use tokio::time::Duration; @@ -61,6 +62,32 @@ const UNIFIED_EXEC_ENV: [(&str, &str); 10] = [ ("CODEX_CI", "1"), ]; +#[cfg(any(test, feature = "test-support"))] +/// Test-only override for deterministic unified exec process IDs. +/// +/// In production builds this value should remain at its default (`false`) and +/// must not be toggled. +static FORCE_DETERMINISTIC_PROCESS_IDS: AtomicBool = AtomicBool::new(false); + +#[cfg(any(test, feature = "test-support"))] +pub(super) fn set_deterministic_process_ids_for_tests(enabled: bool) { + FORCE_DETERMINISTIC_PROCESS_IDS.store(enabled, Ordering::Relaxed); +} + +#[cfg(any(test, feature = "test-support"))] +fn deterministic_process_ids_forced_for_tests() -> bool { + FORCE_DETERMINISTIC_PROCESS_IDS.load(Ordering::Relaxed) +} + +#[cfg(not(any(test, feature = "test-support")))] +fn deterministic_process_ids_forced_for_tests() -> bool { + false +} + +fn should_use_deterministic_process_ids() -> bool { + cfg!(test) || deterministic_process_ids_forced_for_tests() +} + fn apply_unified_exec_env(mut env: HashMap) -> HashMap { for (key, value) in UNIFIED_EXEC_ENV { env.insert(key.to_string(), value.to_string()); @@ -85,10 +112,7 @@ impl UnifiedExecProcessManager { loop { let mut store = self.process_store.lock().await; - let process_id = if !cfg!(test) && !cfg!(feature = "deterministic_process_ids") { - // production mode → random - rand::rng().random_range(1_000..100_000).to_string() - } else { + let process_id = if should_use_deterministic_process_ids() { // test or deterministic mode let next = store .reserved_process_ids @@ -99,6 +123,9 @@ impl UnifiedExecProcessManager { .unwrap_or(1000); next.to_string() + } else { + // production mode → random + rand::rng().random_range(1_000..100_000).to_string() }; if store.reserved_process_ids.contains(&process_id) { diff --git a/codex-rs/core/tests/common/Cargo.toml b/codex-rs/core/tests/common/Cargo.toml index 1c76e5a16e..1fb43bb768 100644 --- a/codex-rs/core/tests/common/Cargo.toml +++ b/codex-rs/core/tests/common/Cargo.toml @@ -15,6 +15,7 @@ codex-core = { workspace = true, features = ["test-support"] } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-cargo-bin = { workspace = true } +ctor = { workspace = true } futures = { workspace = true } notify = { workspace = true } regex-lite = { workspace = true } diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 3feadb0722..0144eeec5a 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -1,6 +1,7 @@ #![expect(clippy::expect_used)] use codex_utils_cargo_bin::CargoBinError; +use ctor::ctor; use tempfile::TempDir; use codex_core::CodexThread; @@ -17,6 +18,11 @@ pub mod streaming_sse; pub mod test_codex; pub mod test_codex_exec; +#[ctor] +fn enable_deterministic_unified_exec_process_ids_for_tests() { + codex_core::set_deterministic_process_ids_for_tests(true); +} + #[track_caller] pub fn assert_regex_match<'s>(pattern: &str, actual: &'s str) -> regex_lite::Captures<'s> { let regex = Regex::new(pattern).unwrap_or_else(|err| {