From f6dd9e37e770cdab2bc11c8798a2c3ac9e31bd2f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 10 Feb 2026 22:23:08 -0800 Subject: [PATCH 1/2] tui: show non-file layer content in /debug-config (#11412) The debug output listed non-file-backed layers such as session flags and MDM managed config, but it did not show their values. That made it difficult to explain unexpected effective settings because users could not inspect those layers on disk. Now `/debug-config` might include output like this: ``` Config layer stack (lowest precedence first): 1. system (/etc/codex/config.toml) (enabled) 2. user (/Users/mbolin/.codex/config.toml) (enabled) 3. legacy managed_config.toml (mdm) (enabled) MDM value: # Production Codex configuration file. [otel] log_user_prompt = true environment = "prod" exporter = { otlp-http = { endpoint = "https://example.com/otel", protocol = "binary" }} ``` --- codex-rs/core/src/config_loader/layer_io.rs | 23 +++- codex-rs/core/src/config_loader/macos.rs | 20 ++- codex-rs/core/src/config_loader/mod.rs | 12 +- codex-rs/core/src/config_loader/state.rs | 18 +++ codex-rs/core/src/config_loader/tests.rs | 31 +++-- codex-rs/tui/src/debug_config.rs | 139 +++++++++++++++++++- 6 files changed, 223 insertions(+), 20 deletions(-) diff --git a/codex-rs/core/src/config_loader/layer_io.rs b/codex-rs/core/src/config_loader/layer_io.rs index f6f7d2e37d..a97f285549 100644 --- a/codex-rs/core/src/config_loader/layer_io.rs +++ b/codex-rs/core/src/config_loader/layer_io.rs @@ -2,6 +2,8 @@ use super::LoaderOverrides; use super::diagnostics::config_error_from_toml; use super::diagnostics::io_error_from_config_error; #[cfg(target_os = "macos")] +use super::macos::ManagedAdminConfigLayer; +#[cfg(target_os = "macos")] use super::macos::load_managed_admin_config_layer; use codex_utils_absolute_path::AbsolutePathBuf; use std::io; @@ -19,12 +21,18 @@ pub(super) struct MangedConfigFromFile { pub file: AbsolutePathBuf, } +#[derive(Debug, Clone)] +pub(super) struct ManagedConfigFromMdm { + pub managed_config: TomlValue, + pub raw_toml: String, +} + #[derive(Debug, Clone)] pub(super) struct LoadedConfigLayers { /// If present, data read from a file such as `/etc/codex/managed_config.toml`. pub managed_config: Option, /// If present, data read from managed preferences (macOS only). - pub managed_config_from_mdm: Option, + pub managed_config_from_mdm: Option, } pub(super) async fn load_config_layers_internal( @@ -57,7 +65,9 @@ pub(super) async fn load_config_layers_internal( #[cfg(target_os = "macos")] let managed_preferences = - load_managed_admin_config_layer(managed_preferences_base64.as_deref()).await?; + load_managed_admin_config_layer(managed_preferences_base64.as_deref()) + .await? + .map(map_managed_admin_layer); #[cfg(not(target_os = "macos"))] let managed_preferences = None; @@ -68,6 +78,15 @@ pub(super) async fn load_config_layers_internal( }) } +#[cfg(target_os = "macos")] +fn map_managed_admin_layer(layer: ManagedAdminConfigLayer) -> ManagedConfigFromMdm { + let ManagedAdminConfigLayer { config, raw_toml } = layer; + ManagedConfigFromMdm { + managed_config: config, + raw_toml, + } +} + pub(super) async fn read_config_from_path( path: impl AsRef, log_missing_as_info: bool, diff --git a/codex-rs/core/src/config_loader/macos.rs b/codex-rs/core/src/config_loader/macos.rs index e3bbcc07e2..b474e81eaa 100644 --- a/codex-rs/core/src/config_loader/macos.rs +++ b/codex-rs/core/src/config_loader/macos.rs @@ -15,6 +15,12 @@ const MANAGED_PREFERENCES_APPLICATION_ID: &str = "com.openai.codex"; const MANAGED_PREFERENCES_CONFIG_KEY: &str = "config_toml_base64"; const MANAGED_PREFERENCES_REQUIREMENTS_KEY: &str = "requirements_toml_base64"; +#[derive(Debug, Clone)] +pub(super) struct ManagedAdminConfigLayer { + pub config: TomlValue, + pub raw_toml: String, +} + pub(super) fn managed_preferences_requirements_source() -> RequirementSource { RequirementSource::MdmManagedPreferences { domain: MANAGED_PREFERENCES_APPLICATION_ID.to_string(), @@ -24,7 +30,7 @@ pub(super) fn managed_preferences_requirements_source() -> RequirementSource { pub(crate) async fn load_managed_admin_config_layer( override_base64: Option<&str>, -) -> io::Result> { +) -> io::Result> { if let Some(encoded) = override_base64 { let trimmed = encoded.trim(); return if trimmed.is_empty() { @@ -47,7 +53,7 @@ pub(crate) async fn load_managed_admin_config_layer( } } -fn load_managed_admin_config() -> io::Result> { +fn load_managed_admin_config() -> io::Result> { load_managed_preference(MANAGED_PREFERENCES_CONFIG_KEY)? .as_deref() .map(str::trim) @@ -122,9 +128,13 @@ fn load_managed_preference(key_name: &str) -> io::Result> { Ok(Some(value)) } -fn parse_managed_config_base64(encoded: &str) -> io::Result { - match toml::from_str::(&decode_managed_preferences_base64(encoded)?) { - Ok(TomlValue::Table(parsed)) => Ok(TomlValue::Table(parsed)), +fn parse_managed_config_base64(encoded: &str) -> io::Result { + let raw_toml = decode_managed_preferences_base64(encoded)?; + match toml::from_str::(&raw_toml) { + Ok(TomlValue::Table(parsed)) => Ok(ManagedAdminConfigLayer { + config: TomlValue::Table(parsed), + raw_toml, + }), Ok(other) => { tracing::error!("Managed config TOML must have a table at the root, found {other:?}",); Err(io::Error::new( diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index a38baf7d47..68973718fe 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -278,9 +278,10 @@ pub async fn load_config_layers_state( )); } if let Some(config) = managed_config_from_mdm { - layers.push(ConfigLayerEntry::new( + layers.push(ConfigLayerEntry::new_with_raw_toml( ConfigLayerSource::LegacyManagedConfigTomlFromMdm, - config, + config.managed_config, + config.raw_toml, )); } @@ -485,7 +486,12 @@ async fn load_requirements_from_legacy_scheme( } = loaded_config_layers; for (source, config) in managed_config_from_mdm - .map(|config| (RequirementSource::LegacyManagedConfigTomlFromMdm, config)) + .map(|config| { + ( + RequirementSource::LegacyManagedConfigTomlFromMdm, + config.managed_config, + ) + }) .into_iter() .chain(managed_config.map(|c| { ( diff --git a/codex-rs/core/src/config_loader/state.rs b/codex-rs/core/src/config_loader/state.rs index 847b19d7e5..30afecc7ca 100644 --- a/codex-rs/core/src/config_loader/state.rs +++ b/codex-rs/core/src/config_loader/state.rs @@ -27,6 +27,7 @@ pub struct LoaderOverrides { pub struct ConfigLayerEntry { pub name: ConfigLayerSource, pub config: TomlValue, + pub raw_toml: Option, pub version: String, pub disabled_reason: Option, } @@ -37,6 +38,18 @@ impl ConfigLayerEntry { Self { name, config, + raw_toml: None, + version, + disabled_reason: None, + } + } + + pub fn new_with_raw_toml(name: ConfigLayerSource, config: TomlValue, raw_toml: String) -> Self { + let version = version_for_toml(&config); + Self { + name, + config, + raw_toml: Some(raw_toml), version, disabled_reason: None, } @@ -51,6 +64,7 @@ impl ConfigLayerEntry { Self { name, config, + raw_toml: None, version, disabled_reason: Some(disabled_reason.into()), } @@ -60,6 +74,10 @@ impl ConfigLayerEntry { self.disabled_reason.is_some() } + pub fn raw_toml(&self) -> Option<&str> { + self.raw_toml.as_deref() + } + pub fn metadata(&self) -> ConfigLayerMetadata { ConfigLayerMetadata { name: self.name.clone(), diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 0a6f67d85b..79c657ad96 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -264,6 +264,7 @@ async fn returns_empty_when_all_layers_missing() { .expect("resolve user config.toml path") }, config: TomlValue::Table(toml::map::Map::new()), + raw_toml: None, version: version_for_toml(&TomlValue::Table(toml::map::Map::new())), disabled_reason: None, }, @@ -325,18 +326,17 @@ flag = true "#, ) .expect("write managed config"); + let raw_managed_preferences = r#" +# managed profile +[nested] +value = "managed" +flag = false +"#; let overrides = LoaderOverrides { managed_config_path: Some(managed_path), managed_preferences_base64: Some( - base64::prelude::BASE64_STANDARD.encode( - r#" -[nested] -value = "managed" -flag = false -"# - .as_bytes(), - ), + base64::prelude::BASE64_STANDARD.encode(raw_managed_preferences.as_bytes()), ), macos_managed_config_requirements_base64: None, }; @@ -361,6 +361,19 @@ flag = false Some(&TomlValue::String("managed".to_string())) ); assert_eq!(nested.get("flag"), Some(&TomlValue::Boolean(false))); + let mdm_layer = state + .layers_high_to_low() + .into_iter() + .find(|layer| { + matches!( + layer.name, + super::ConfigLayerSource::LegacyManagedConfigTomlFromMdm + ) + }) + .expect("mdm layer"); + let raw = mdm_layer.raw_toml().expect("preserved mdm toml"); + assert!(raw.contains("# managed profile")); + assert!(raw.contains("value = \"managed\"")); } #[cfg(target_os = "macos")] @@ -862,6 +875,7 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s dot_codex_folder: AbsolutePathBuf::from_absolute_path(project_root.join(".codex"))?, }, config: TomlValue::Table(toml::map::Map::new()), + raw_toml: None, version: version_for_toml(&TomlValue::Table(toml::map::Map::new())), disabled_reason: None, }], @@ -955,6 +969,7 @@ async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Resul dot_codex_folder: AbsolutePathBuf::from_absolute_path(&nested_dot_codex)?, }, config: child_config.clone(), + raw_toml: None, version: version_for_toml(&child_config), disabled_reason: None, }], diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 681d9c3a9a..01f5cf6cac 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -1,6 +1,7 @@ use crate::history_cell::PlainHistoryCell; use codex_app_server_protocol::ConfigLayerSource; use codex_core::config::Config; +use codex_core::config_loader::ConfigLayerEntry; use codex_core::config_loader::ConfigLayerStack; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::config_loader::NetworkConstraints; @@ -11,6 +12,7 @@ use codex_core::config_loader::WebSearchModeRequirement; use codex_core::protocol::SessionNetworkProxyRuntime; use ratatui::style::Stylize; use ratatui::text::Line; +use toml::Value as TomlValue; pub(crate) fn new_debug_config_output( config: &Config, @@ -71,6 +73,7 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec> { "enabled" }; lines.push(format!(" {}. {source} ({status})", index + 1).into()); + lines.extend(render_non_file_layer_details(layer)); if let Some(reason) = &layer.disabled_reason { lines.push(format!(" reason: {reason}").dim().into()); } @@ -169,6 +172,80 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec> { lines } +fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec> { + match &layer.name { + ConfigLayerSource::SessionFlags => render_session_flag_details(&layer.config), + ConfigLayerSource::Mdm { .. } | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => { + render_mdm_layer_details(layer) + } + ConfigLayerSource::System { .. } + | ConfigLayerSource::User { .. } + | ConfigLayerSource::Project { .. } + | ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => Vec::new(), + } +} + +fn render_session_flag_details(config: &TomlValue) -> Vec> { + let mut pairs = Vec::new(); + flatten_toml_key_values(config, None, &mut pairs); + + if pairs.is_empty() { + return vec![" - ".dim().into()]; + } + + pairs + .into_iter() + .map(|(key, value)| format!(" - {key} = {value}").into()) + .collect() +} + +fn render_mdm_layer_details(layer: &ConfigLayerEntry) -> Vec> { + let value = layer + .raw_toml() + .map(ToString::to_string) + .unwrap_or_else(|| format_toml_value(&layer.config)); + if value.is_empty() { + return vec![" MDM value: ".dim().into()]; + } + + if value.contains('\n') { + let mut lines = vec![" MDM value:".into()]; + lines.extend(value.lines().map(|line| format!(" {line}").into())); + lines + } else { + vec![format!(" MDM value: {value}").into()] + } +} + +fn flatten_toml_key_values( + value: &TomlValue, + prefix: Option<&str>, + out: &mut Vec<(String, String)>, +) { + match value { + TomlValue::Table(table) => { + let mut entries = table.iter().collect::>(); + entries.sort_by_key(|(key, _)| key.as_str()); + for (key, child) in entries { + let next_prefix = if let Some(prefix) = prefix { + format!("{prefix}.{key}") + } else { + key.to_string() + }; + flatten_toml_key_values(child, Some(&next_prefix), out); + } + } + _ => { + let key = prefix.unwrap_or("").to_string(); + out.push((key, format_toml_value(value))); + } + } +} + +fn format_toml_value(value: &TomlValue) -> String { + value.to_string() +} + fn requirement_line( name: &str, value: String, @@ -205,7 +282,7 @@ fn normalize_allowed_web_search_modes( fn format_config_layer_source(source: &ConfigLayerSource) -> String { match source { ConfigLayerSource::Mdm { domain, key } => { - format!("mdm ({domain}:{key})") + format!("MDM ({domain}:{key})") } ConfigLayerSource::System { file } => { format!("system ({})", file.as_path().display()) @@ -224,7 +301,7 @@ fn format_config_layer_source(source: &ConfigLayerSource) -> String { format!("legacy managed_config.toml ({})", file.as_path().display()) } ConfigLayerSource::LegacyManagedConfigTomlFromMdm => { - "legacy managed_config.toml (mdm)".to_string() + "legacy managed_config.toml (MDM)".to_string() } } } @@ -494,6 +571,64 @@ mod tests { assert!(!rendered.contains(" - rules:")); } + #[test] + fn debug_config_output_lists_session_flag_key_value_pairs() { + let session_flags = toml::from_str::( + r#" +model = "gpt-5" +[sandbox_workspace_write] +network_access = true +writable_roots = ["/tmp"] +"#, + ) + .expect("session flags"); + + let stack = ConfigLayerStack::new( + vec![ConfigLayerEntry::new( + ConfigLayerSource::SessionFlags, + session_flags, + )], + ConfigRequirements::default(), + ConfigRequirementsToml::default(), + ) + .expect("config layer stack"); + + let rendered = render_to_text(&render_debug_config_lines(&stack)); + assert!(rendered.contains("session-flags (enabled)")); + assert!(rendered.contains(" - model = \"gpt-5\"")); + assert!(rendered.contains(" - sandbox_workspace_write.network_access = true")); + assert!(rendered.contains("sandbox_workspace_write.writable_roots")); + assert!(rendered.contains("/tmp")); + } + + #[test] + fn debug_config_output_shows_legacy_mdm_layer_value() { + let raw_mdm_toml = r#" +# managed by MDM +model = "managed_model" +approval_policy = "never" +"#; + let mdm_value = toml::from_str::(raw_mdm_toml).expect("MDM value"); + + let stack = ConfigLayerStack::new( + vec![ConfigLayerEntry::new_with_raw_toml( + ConfigLayerSource::LegacyManagedConfigTomlFromMdm, + mdm_value, + raw_mdm_toml.to_string(), + )], + ConfigRequirements::default(), + ConfigRequirementsToml::default(), + ) + .expect("config layer stack"); + + let rendered = render_to_text(&render_debug_config_lines(&stack)); + assert!(rendered.contains("legacy managed_config.toml (MDM) (enabled)")); + assert!(rendered.contains("MDM value:")); + assert!(rendered.contains("# managed by MDM")); + assert!(rendered.contains("model = \"managed_model\"")); + assert!(rendered.contains("approval_policy = \"never\"")); + } + #[test] fn debug_config_output_normalizes_empty_web_search_mode_list() { let mut requirements = ConfigRequirements::default(); From b50aedf8ccd9a8cfa68fc1527541fad5f0c1c650 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 10 Feb 2026 22:24:44 -0800 Subject: [PATCH 2/2] Remove `test-support` feature from `codex-core` and replace it with explicit test toggles ## Why `codex-core` still had a `test-support` crate feature that was enabled by multiple consumers (`core_test_support`, `app_test_support`, and `codex-tui` dev-deps). That introduced a second feature-resolved shape of `codex-core`, which increases compile cost and cache fragmentation across the workspace. The same root issue previously affected `deterministic_process_ids`: we were using crate features as a test/runtime switch, and Cargo had to build extra variants of a very large crate. ## What Changed ### 1) Remove `test-support` as a crate feature - Deleted the feature declaration from `core/Cargo.toml`. - Removed `features = ["test-support"]` from: - `core/tests/common/Cargo.toml` - `app-server/tests/common/Cargo.toml` - `tui/Cargo.toml` (dev-dependency) - Removed Bazel `crate_features = ["test-support"]` from `core/BUILD.bazel`. ### 2) Keep test behavior without feature-gated crate variants - Converted test-only behavior toggles to **explicit runtime switches** backed by `AtomicBool`: - thread-manager test mode toggle - deterministic unified-exec process-id toggle - Enabled those toggles from `core_test_support` ctor so integration tests keep deterministic and test-friendly behavior. ### 3) Replace feature-gated helper APIs with always-compiled hidden helpers - APIs that were previously behind `#[cfg(feature = "test-support")]` are now available as `#[doc(hidden)]` test helpers, avoiding feature-split builds while preserving existing test call sites. ## Expected Benefits ### Concrete dependency-graph effect `cargo tree -p codex-core -e features` now shows only the default feature path for `codex-core`; there are no `test-support` or `deterministic_process_ids` feature edges remaining. ### Build-performance impact - Eliminates feature-driven duplicate `codex-core` compilation variants. - Improves cache reuse across test-support consumers that previously forced separate feature resolution. - Reduces rebuild churn when switching between targets that did and did not depend on the feature-enabled `codex-core` shape. ## Safety / behavior notes - Production behavior remains unchanged by default. - Test-only behavior is now explicit and opt-in via dedicated test toggles, with docstrings clarifying these must stay at default values in production. ## Validation - `just fmt` - `cargo test -p codex-core unified_exec::` - `cargo test -p codex-core --test all unified_exec -- --test-threads=1` - `cargo check -p app_test_support` - `cargo check -p codex-tui --tests` - `cargo check -p codex-core` - `cargo tree -p codex-core -e features` --- codex-rs/app-server/tests/common/Cargo.toml | 2 +- .../app-server/tests/common/models_cache.rs | 2 +- .../tests/suite/v2/collaboration_mode_list.rs | 6 +- codex-rs/core/BUILD.bazel | 4 - codex-rs/core/Cargo.toml | 4 - codex-rs/core/src/agent/control.rs | 12 +-- codex-rs/core/src/auth.rs | 9 +- codex-rs/core/src/codex.rs | 31 +++--- codex-rs/core/src/lib.rs | 3 +- .../core/src/memories/startup/dispatch.rs | 2 +- .../collaboration_mode_presets.rs | 7 +- codex-rs/core/src/models_manager/manager.rs | 61 ++++++++---- codex-rs/core/src/models_manager/mod.rs | 3 - .../core/src/models_manager/model_presets.rs | 7 +- codex-rs/core/src/test_support.rs | 78 +++++++++++++++ codex-rs/core/src/thread_manager.rs | 97 ++++++++++++------- codex-rs/core/src/tools/handlers/collab.rs | 2 +- codex-rs/core/src/tools/spec.rs | 54 ++++++----- codex-rs/core/src/unified_exec/mod.rs | 3 +- .../core/src/unified_exec/process_manager.rs | 8 -- codex-rs/core/tests/common/Cargo.toml | 2 +- codex-rs/core/tests/common/lib.rs | 3 +- codex-rs/core/tests/common/test_codex.rs | 4 +- codex-rs/core/tests/responses_headers.rs | 22 +++-- codex-rs/core/tests/suite/client.rs | 12 +-- .../core/tests/suite/client_websockets.rs | 7 +- .../core/tests/suite/compact_resume_fork.rs | 2 +- .../core/tests/suite/model_info_overrides.rs | 4 +- codex-rs/core/tests/suite/personality.rs | 8 +- codex-rs/core/tests/suite/remote_models.rs | 28 +++--- codex-rs/core/tests/suite/resume_warning.rs | 7 +- .../tests/suite/unstable_features_warning.rs | 12 +-- codex-rs/tui/Cargo.toml | 2 +- codex-rs/tui/src/app.rs | 37 +++---- codex-rs/tui/src/chatwidget/tests.rs | 81 +++++++++------- codex-rs/tui/src/status/tests.rs | 33 +++---- 36 files changed, 393 insertions(+), 266 deletions(-) create mode 100644 codex-rs/core/src/test_support.rs diff --git a/codex-rs/app-server/tests/common/Cargo.toml b/codex-rs/app-server/tests/common/Cargo.toml index d91ed4e675..de58509f0d 100644 --- a/codex-rs/app-server/tests/common/Cargo.toml +++ b/codex-rs/app-server/tests/common/Cargo.toml @@ -12,7 +12,7 @@ anyhow = { workspace = true } base64 = { workspace = true } chrono = { workspace = true } codex-app-server-protocol = { workspace = true } -codex-core = { workspace = true, features = ["test-support"] } +codex-core = { workspace = true } codex-protocol = { workspace = true } codex-utils-cargo-bin = { workspace = true } serde = { workspace = true } diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 00359b28d8..242a71f98d 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -1,6 +1,6 @@ use chrono::DateTime; use chrono::Utc; -use codex_core::models_manager::model_presets::all_model_presets; +use codex_core::test_support::all_model_presets; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; diff --git a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs index 4837f68cd6..4435542909 100644 --- a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs +++ b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs @@ -15,7 +15,7 @@ use codex_app_server_protocol::CollaborationModeListParams; use codex_app_server_protocol::CollaborationModeListResponse; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; -use codex_core::models_manager::test_builtin_collaboration_mode_presets; +use codex_core::test_support::builtin_collaboration_mode_presets; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; use pretty_assertions::assert_eq; @@ -55,7 +55,7 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> { /// If the defaults change in the app server, this helper should be updated alongside the /// contract, or the test will fail in ways that imply a regression in the API. fn plan_preset() -> CollaborationModeMask { - let presets = test_builtin_collaboration_mode_presets(); + let presets = builtin_collaboration_mode_presets(); presets .into_iter() .find(|p| p.mode == Some(ModeKind::Plan)) @@ -64,7 +64,7 @@ fn plan_preset() -> CollaborationModeMask { /// Builds the default preset that the list response is expected to return. fn default_preset() -> CollaborationModeMask { - let presets = test_builtin_collaboration_mode_presets(); + let presets = builtin_collaboration_mode_presets(); presets .into_iter() .find(|p| p.mode == Some(ModeKind::Default)) diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index 6e5ce680ae..2ef299c5c0 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -3,10 +3,6 @@ load("//:defs.bzl", "codex_rust_crate") codex_rust_crate( name = "core", crate_name = "codex_core", - # 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 = ["test-support"], compile_data = glob( include = ["**"], exclude = [ diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 26e7f17cc8..e759072fac 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -113,10 +113,6 @@ which = { workspace = true } wildmatch = { workspace = true } zip = { workspace = true } -[features] -test-support = [] - - [target.'cfg(target_os = "linux")'.dependencies] keyring = { workspace = true, features = ["linux-native-async-persistent"] } landlock = { workspace = true } diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index c2dc3b8f88..b0164e86e6 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -215,7 +215,7 @@ mod tests { impl AgentControlHarness { async fn new() -> Self { let (home, config) = test_config().await; - let manager = ThreadManager::with_models_provider_and_home( + let manager = ThreadManager::with_models_provider_and_home_for_tests( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.clone(), @@ -484,7 +484,7 @@ mod tests { TomlValue::Integer(max_threads as i64), )]) .await; - let manager = ThreadManager::with_models_provider_and_home( + let manager = ThreadManager::with_models_provider_and_home_for_tests( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.clone(), @@ -527,7 +527,7 @@ mod tests { TomlValue::Integer(max_threads as i64), )]) .await; - let manager = ThreadManager::with_models_provider_and_home( + let manager = ThreadManager::with_models_provider_and_home_for_tests( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.clone(), @@ -561,7 +561,7 @@ mod tests { TomlValue::Integer(max_threads as i64), )]) .await; - let manager = ThreadManager::with_models_provider_and_home( + let manager = ThreadManager::with_models_provider_and_home_for_tests( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.clone(), @@ -597,7 +597,7 @@ mod tests { TomlValue::Integer(max_threads as i64), )]) .await; - let manager = ThreadManager::with_models_provider_and_home( + let manager = ThreadManager::with_models_provider_and_home_for_tests( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.clone(), @@ -650,7 +650,7 @@ mod tests { TomlValue::Integer(max_threads as i64), )]) .await; - let manager = ThreadManager::with_models_provider_and_home( + let manager = ThreadManager::with_models_provider_and_home_for_tests( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.clone(), diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index a6282305ea..2cb2ce51fa 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -981,9 +981,8 @@ impl AuthManager { } } - #[cfg(any(test, feature = "test-support"))] /// Create an AuthManager with a specific CodexAuth, for testing only. - pub fn from_auth_for_testing(auth: CodexAuth) -> Arc { + pub(crate) fn from_auth_for_testing(auth: CodexAuth) -> Arc { let cached = CachedAuth { auth: Some(auth), external_refresher: None, @@ -998,9 +997,11 @@ impl AuthManager { }) } - #[cfg(any(test, feature = "test-support"))] /// Create an AuthManager with a specific CodexAuth and codex home, for testing only. - pub fn from_auth_for_testing_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc { + pub(crate) fn from_auth_for_testing_with_home( + auth: CodexAuth, + codex_home: PathBuf, + ) -> Arc { let cached = CachedAuth { auth: Some(auth), external_refresher: None, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 5b88d38d52..e4748d98f2 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -5787,8 +5787,9 @@ mod tests { let codex_home = tempfile::tempdir().expect("create temp dir"); let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); - let model = ModelsManager::get_model_offline(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); + let model_info = + ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -5876,8 +5877,9 @@ mod tests { let codex_home = tempfile::tempdir().expect("create temp dir"); let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); - let model = ModelsManager::get_model_offline(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); + let model_info = + ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -6129,7 +6131,7 @@ mod tests { ) -> OtelManager { OtelManager::new( conversation_id, - ModelsManager::get_model_offline(config.model.as_deref()).as_str(), + ModelsManager::get_model_offline_for_tests(config.model.as_deref()).as_str(), model_info.slug.as_str(), None, Some("test@test.com".to_string()), @@ -6145,8 +6147,9 @@ mod tests { let codex_home = tempfile::tempdir().expect("create temp dir"); let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); - let model = ModelsManager::get_model_offline(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); + let model_info = + ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -6197,8 +6200,9 @@ mod tests { let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit); - let model = ModelsManager::get_model_offline(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); + let model_info = + ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -6231,7 +6235,7 @@ mod tests { dynamic_tools: Vec::new(), }; let per_turn_config = Session::build_per_turn_config(&session_configuration); - let model_info = ModelsManager::construct_model_info_offline( + let model_info = ModelsManager::construct_model_info_offline_for_tests( session_configuration.collaboration_mode.model(), &per_turn_config, ); @@ -6333,8 +6337,9 @@ mod tests { let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit); - let model = ModelsManager::get_model_offline(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); + let model_info = + ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -6367,7 +6372,7 @@ mod tests { dynamic_tools: Vec::new(), }; let per_turn_config = Session::build_per_turn_config(&session_configuration); - let model_info = ModelsManager::construct_model_info_offline( + let model_info = ModelsManager::construct_model_info_offline_for_tests( session_configuration.collaboration_mode.model(), &per_turn_config, ); diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 85095bc5f5..53ac2e0d88 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -59,6 +59,7 @@ mod session_prefix; mod shell_detect; mod stream_events_utils; mod tagged_block_parser; +pub mod test_support; mod text_encoding; pub mod token_data; mod truncate; @@ -145,8 +146,6 @@ 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/memories/startup/dispatch.rs b/codex-rs/core/src/memories/startup/dispatch.rs index ebc39bec98..58050c1ed1 100644 --- a/codex-rs/core/src/memories/startup/dispatch.rs +++ b/codex-rs/core/src/memories/startup/dispatch.rs @@ -208,7 +208,7 @@ mod tests { .await .expect("initialize state db"); - let manager = ThreadManager::with_models_provider_and_home( + let manager = ThreadManager::with_models_provider_and_home_for_tests( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.clone(), diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index cdc5134038..25f083cb81 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -9,15 +9,10 @@ const COLLABORATION_MODE_DEFAULT: &str = const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; -pub(super) fn builtin_collaboration_mode_presets() -> Vec { +pub(crate) fn builtin_collaboration_mode_presets() -> Vec { vec![plan_preset(), default_preset()] } -#[cfg(any(test, feature = "test-support"))] -pub fn test_builtin_collaboration_mode_presets() -> Vec { - builtin_collaboration_mode_presets() -} - fn plan_preset() -> CollaborationModeMask { CollaborationModeMask { name: ModeKind::Plan.display_name().to_string(), diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index d3bf497e63..5c6a5db7fc 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -336,9 +336,8 @@ impl ModelsManager { } } - #[cfg(any(test, feature = "test-support"))] /// Construct a manager with a specific provider for testing. - pub fn with_provider( + pub(crate) fn with_provider_for_tests( codex_home: PathBuf, auth_manager: Arc, provider: ModelProviderInfo, @@ -355,9 +354,8 @@ impl ModelsManager { } } - #[cfg(any(test, feature = "test-support"))] /// Get model identifier without consulting remote state or cache. - pub fn get_model_offline(model: Option<&str>) -> String { + pub(crate) fn get_model_offline_for_tests(model: Option<&str>) -> String { if let Some(model) = model { return model.to_string(); } @@ -370,9 +368,11 @@ impl ModelsManager { .unwrap_or_default() } - #[cfg(any(test, feature = "test-support"))] /// Build `ModelInfo` without consulting remote state or cache. - pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo { + pub(crate) fn construct_model_info_offline_for_tests( + model: &str, + config: &Config, + ) -> ModelInfo { model_info::with_config_overrides(model_info::model_info_from_slug(model), config) } } @@ -482,8 +482,11 @@ mod tests { let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); - let manager = - ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); + let manager = ModelsManager::with_provider_for_tests( + codex_home.path().to_path_buf(), + auth_manager, + provider, + ); manager .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) @@ -536,8 +539,11 @@ mod tests { let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); - let manager = - ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); + let manager = ModelsManager::with_provider_for_tests( + codex_home.path().to_path_buf(), + auth_manager, + provider, + ); manager .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) @@ -580,8 +586,11 @@ mod tests { let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); - let manager = - ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); + let manager = ModelsManager::with_provider_for_tests( + codex_home.path().to_path_buf(), + auth_manager, + provider, + ); manager .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) @@ -646,8 +655,11 @@ mod tests { let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); - let manager = - ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); + let manager = ModelsManager::with_provider_for_tests( + codex_home.path().to_path_buf(), + auth_manager, + provider, + ); manager .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) @@ -712,8 +724,11 @@ mod tests { let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); - let mut manager = - ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); + let mut manager = ModelsManager::with_provider_for_tests( + codex_home.path().to_path_buf(), + auth_manager, + provider, + ); manager.cache_manager.set_ttl(Duration::ZERO); manager @@ -784,8 +799,11 @@ mod tests { AuthCredentialsStoreMode::File, )); let provider = provider_for(server.uri()); - let manager = - ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); + let manager = ModelsManager::with_provider_for_tests( + codex_home.path().to_path_buf(), + auth_manager, + provider, + ); manager .refresh_available_models(&config, RefreshStrategy::Online) @@ -811,8 +829,11 @@ mod tests { let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let provider = provider_for("http://example.test".to_string()); - let mut manager = - ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); + let mut manager = ModelsManager::with_provider_for_tests( + codex_home.path().to_path_buf(), + auth_manager, + provider, + ); manager.local_models = Vec::new(); let hidden_model = remote_model_with_visibility("hidden", "Hidden", 0, "hide"); diff --git a/codex-rs/core/src/models_manager/mod.rs b/codex-rs/core/src/models_manager/mod.rs index 9527466252..53b33bc4e5 100644 --- a/codex-rs/core/src/models_manager/mod.rs +++ b/codex-rs/core/src/models_manager/mod.rs @@ -4,9 +4,6 @@ pub mod manager; pub mod model_info; pub mod model_presets; -#[cfg(any(test, feature = "test-support"))] -pub use collaboration_mode_presets::test_builtin_collaboration_mode_presets; - /// Convert the client version string to a whole version string (e.g. "1.2.3-alpha.4" -> "1.2.3"). pub fn client_version_to_whole() -> String { format!( diff --git a/codex-rs/core/src/models_manager/model_presets.rs b/codex-rs/core/src/models_manager/model_presets.rs index a597f7f922..dd24c1829f 100644 --- a/codex-rs/core/src/models_manager/model_presets.rs +++ b/codex-rs/core/src/models_manager/model_presets.rs @@ -11,7 +11,7 @@ pub const HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt5_1_migration_pro pub const HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt-5.1-codex-max_migration_prompt"; -static PRESETS: Lazy> = Lazy::new(|| { +pub(crate) static PRESETS: Lazy> = Lazy::new(|| { vec![ ModelPreset { id: "gpt-5.2-codex".to_string(), @@ -359,11 +359,6 @@ pub(super) fn builtin_model_presets(_auth_mode: Option) -> Vec &'static Vec { - &PRESETS -} - #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/core/src/test_support.rs b/codex-rs/core/src/test_support.rs new file mode 100644 index 0000000000..c216ee2f80 --- /dev/null +++ b/codex-rs/core/src/test_support.rs @@ -0,0 +1,78 @@ +//! Test-only helpers exposed for cross-crate integration tests. +//! +//! Production code should not depend on this module. +//! We prefer this to using a crate feature to avoid building multiple +//! permutations of the crate. + +use std::path::PathBuf; +use std::sync::Arc; + +use codex_protocol::config_types::CollaborationModeMask; +use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ModelPreset; + +use crate::AuthManager; +use crate::CodexAuth; +use crate::ModelProviderInfo; +use crate::ThreadManager; +use crate::config::Config; +use crate::models_manager::collaboration_mode_presets; +use crate::models_manager::manager::ModelsManager; +use crate::models_manager::model_presets; +use crate::thread_manager; +use crate::unified_exec; + +pub fn set_thread_manager_test_mode(enabled: bool) { + thread_manager::set_thread_manager_test_mode_for_tests(enabled); +} + +pub fn set_deterministic_process_ids(enabled: bool) { + unified_exec::set_deterministic_process_ids_for_tests(enabled); +} + +pub fn auth_manager_from_auth(auth: CodexAuth) -> Arc { + AuthManager::from_auth_for_testing(auth) +} + +pub fn auth_manager_from_auth_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc { + AuthManager::from_auth_for_testing_with_home(auth, codex_home) +} + +pub fn thread_manager_with_models_provider( + auth: CodexAuth, + provider: ModelProviderInfo, +) -> ThreadManager { + ThreadManager::with_models_provider_for_tests(auth, provider) +} + +pub fn thread_manager_with_models_provider_and_home( + auth: CodexAuth, + provider: ModelProviderInfo, + codex_home: PathBuf, +) -> ThreadManager { + ThreadManager::with_models_provider_and_home_for_tests(auth, provider, codex_home) +} + +pub fn models_manager_with_provider( + codex_home: PathBuf, + auth_manager: Arc, + provider: ModelProviderInfo, +) -> ModelsManager { + ModelsManager::with_provider_for_tests(codex_home, auth_manager, provider) +} + +pub fn get_model_offline(model: Option<&str>) -> String { + ModelsManager::get_model_offline_for_tests(model) +} + +pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo { + ModelsManager::construct_model_info_offline_for_tests(model, config) +} + +pub fn all_model_presets() -> &'static Vec { + &model_presets::PRESETS +} + +pub fn builtin_collaboration_mode_presets() -> Vec { + collaboration_mode_presets::builtin_collaboration_mode_presets() +} diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 13f9efafb0..8117f62eca 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -1,7 +1,5 @@ use crate::AuthManager; -#[cfg(any(test, feature = "test-support"))] use crate::CodexAuth; -#[cfg(any(test, feature = "test-support"))] use crate::ModelProviderInfo; use crate::agent::AgentControl; use crate::codex::Codex; @@ -31,25 +29,50 @@ use codex_protocol::protocol::SessionSource; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; -#[cfg(any(test, feature = "test-support"))] -use tempfile::TempDir; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; use tokio::runtime::Handle; -#[cfg(any(test, feature = "test-support"))] use tokio::runtime::RuntimeFlavor; use tokio::sync::RwLock; use tokio::sync::broadcast; use tracing::warn; const THREAD_CREATED_CHANNEL_CAPACITY: usize = 1024; +/// Test-only override for enabling thread-manager behaviors used by integration +/// tests. +/// +/// In production builds this value should remain at its default (`false`) and +/// must not be toggled. +static FORCE_TEST_THREAD_MANAGER_BEHAVIOR: AtomicBool = AtomicBool::new(false); + +type CapturedOps = Vec<(ThreadId, Op)>; +type SharedCapturedOps = Arc>; + +pub(crate) fn set_thread_manager_test_mode_for_tests(enabled: bool) { + FORCE_TEST_THREAD_MANAGER_BEHAVIOR.store(enabled, Ordering::Relaxed); +} + +fn should_use_test_thread_manager_behavior() -> bool { + FORCE_TEST_THREAD_MANAGER_BEHAVIOR.load(Ordering::Relaxed) +} + +struct TempCodexHomeGuard { + path: PathBuf, +} + +impl Drop for TempCodexHomeGuard { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.path); + } +} fn build_file_watcher(codex_home: PathBuf, skills_manager: Arc) -> Arc { - #[cfg(any(test, feature = "test-support"))] - if let Ok(handle) = Handle::try_current() + if should_use_test_thread_manager_behavior() + && let Ok(handle) = Handle::try_current() && handle.runtime_flavor() == RuntimeFlavor::CurrentThread { // The real watcher spins background tasks that can starve the // current-thread test runtime and cause event waits to time out. - // Integration tests compile with the `test-support` feature. warn!("using noop file watcher under current-thread test runtime"); return Arc::new(FileWatcher::noop()); } @@ -95,8 +118,7 @@ pub struct NewThread { /// them in memory. pub struct ThreadManager { state: Arc, - #[cfg(any(test, feature = "test-support"))] - _test_codex_home_guard: Option, + _test_codex_home_guard: Option, } /// Shared, `Arc`-owned state for [`ThreadManager`]. This `Arc` is required to have a single @@ -110,10 +132,8 @@ pub(crate) struct ThreadManagerState { skills_manager: Arc, file_watcher: Arc, session_source: SessionSource, - #[cfg(any(test, feature = "test-support"))] - #[allow(dead_code)] - // Captures submitted ops for testing purpose. - ops_log: Arc>>, + // Captures submitted ops for testing purpose when test mode is enabled. + ops_log: Option, } impl ThreadManager { @@ -134,33 +154,40 @@ impl ThreadManager { file_watcher, auth_manager, session_source, - #[cfg(any(test, feature = "test-support"))] - ops_log: Arc::new(std::sync::Mutex::new(Vec::new())), + ops_log: should_use_test_thread_manager_behavior() + .then(|| Arc::new(std::sync::Mutex::new(Vec::new()))), }), - #[cfg(any(test, feature = "test-support"))] _test_codex_home_guard: None, } } - #[cfg(any(test, feature = "test-support"))] /// Construct with a dummy AuthManager containing the provided CodexAuth. /// Used for integration tests: should not be used by ordinary business logic. - pub fn with_models_provider(auth: CodexAuth, provider: ModelProviderInfo) -> Self { - let temp_dir = tempfile::tempdir().unwrap_or_else(|err| panic!("temp codex home: {err}")); - let codex_home = temp_dir.path().to_path_buf(); - let mut manager = Self::with_models_provider_and_home(auth, provider, codex_home); - manager._test_codex_home_guard = Some(temp_dir); + pub(crate) fn with_models_provider_for_tests( + auth: CodexAuth, + provider: ModelProviderInfo, + ) -> Self { + set_thread_manager_test_mode_for_tests(true); + let codex_home = std::env::temp_dir().join(format!( + "codex-thread-manager-test-{}", + uuid::Uuid::new_v4() + )); + std::fs::create_dir_all(&codex_home) + .unwrap_or_else(|err| panic!("temp codex home dir create failed: {err}")); + let mut manager = + Self::with_models_provider_and_home_for_tests(auth, provider, codex_home.clone()); + manager._test_codex_home_guard = Some(TempCodexHomeGuard { path: codex_home }); manager } - #[cfg(any(test, feature = "test-support"))] /// Construct with a dummy AuthManager containing the provided CodexAuth and codex home. /// Used for integration tests: should not be used by ordinary business logic. - pub fn with_models_provider_and_home( + pub(crate) fn with_models_provider_and_home_for_tests( auth: CodexAuth, provider: ModelProviderInfo, codex_home: PathBuf, ) -> Self { + set_thread_manager_test_mode_for_tests(true); let auth_manager = AuthManager::from_auth_for_testing(auth); let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY); let skills_manager = Arc::new(SkillsManager::new(codex_home.clone())); @@ -169,7 +196,7 @@ impl ThreadManager { state: Arc::new(ThreadManagerState { threads: Arc::new(RwLock::new(HashMap::new())), thread_created_tx, - models_manager: Arc::new(ModelsManager::with_provider( + models_manager: Arc::new(ModelsManager::with_provider_for_tests( codex_home, auth_manager.clone(), provider, @@ -178,8 +205,8 @@ impl ThreadManager { file_watcher, auth_manager, session_source: SessionSource::Exec, - #[cfg(any(test, feature = "test-support"))] - ops_log: Arc::new(std::sync::Mutex::new(Vec::new())), + ops_log: should_use_test_thread_manager_behavior() + .then(|| Arc::new(std::sync::Mutex::new(Vec::new()))), }), _test_codex_home_guard: None, } @@ -340,13 +367,12 @@ impl ThreadManager { AgentControl::new(Arc::downgrade(&self.state)) } - #[cfg(any(test, feature = "test-support"))] - #[allow(dead_code)] + #[cfg(test)] pub(crate) fn captured_ops(&self) -> Vec<(ThreadId, Op)> { self.state .ops_log - .lock() - .map(|log| log.clone()) + .as_ref() + .and_then(|ops_log| ops_log.lock().ok().map(|log| log.clone())) .unwrap_or_default() } } @@ -364,11 +390,10 @@ impl ThreadManagerState { /// Send an operation to a thread by ID. pub(crate) async fn send_op(&self, thread_id: ThreadId, op: Op) -> CodexResult { let thread = self.get_thread(thread_id).await?; - #[cfg(any(test, feature = "test-support"))] + if let Some(ops_log) = &self.ops_log + && let Ok(mut log) = ops_log.lock() { - if let Ok(mut log) = self.ops_log.lock() { - log.push((thread_id, op.clone())); - } + log.push((thread_id, op.clone())); } thread.submit(op).await } diff --git a/codex-rs/core/src/tools/handlers/collab.rs b/codex-rs/core/src/tools/handlers/collab.rs index d104100685..92780eee88 100644 --- a/codex-rs/core/src/tools/handlers/collab.rs +++ b/codex-rs/core/src/tools/handlers/collab.rs @@ -895,7 +895,7 @@ mod tests { } fn thread_manager() -> ThreadManager { - ThreadManager::with_models_provider( + ThreadManager::with_models_provider_for_tests( CodexAuth::from_api_key("dummy"), built_in_model_providers()["openai"].clone(), ) diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 7254252eb4..f136e75563 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -165,10 +165,10 @@ fn create_approval_parameters(include_prefix_rule: bool) -> BTreeMap BTreeMap ToolSpec { let description = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"]. - + Examples of valid command strings: - ls -a (show hidden): ["powershell.exe", "-Command", "Get-ChildItem -Force"] @@ -402,7 +402,7 @@ fn create_shell_command_tool(include_prefix_rule: bool) -> ToolSpec { let description = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. - + Examples of valid command strings: - ls -a (show hidden): "Get-ChildItem -Force" @@ -1766,7 +1766,8 @@ mod tests { #[test] fn test_build_specs_collab_tools_enabled() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); features.enable(Feature::CollaborationModes); @@ -1791,7 +1792,8 @@ mod tests { #[test] fn request_user_input_requires_collaboration_modes_feature() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.disable(Feature::CollaborationModes); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -1851,7 +1853,8 @@ mod tests { #[test] fn web_search_mode_cached_sets_external_web_access_false() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -1873,7 +1876,8 @@ mod tests { #[test] fn web_search_mode_live_sets_external_web_access_true() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -2098,7 +2102,7 @@ mod tests { #[test] fn test_build_specs_default_shell_present() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("o3", &config); + let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -2120,7 +2124,8 @@ mod tests { #[ignore] fn test_parallel_support_flags() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -2175,7 +2180,7 @@ mod tests { #[test] fn test_build_specs_mcp_tools_converted() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("o3", &config); + let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -2260,7 +2265,7 @@ mod tests { #[test] fn test_build_specs_mcp_tools_sorted_by_name() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("o3", &config); + let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -2304,7 +2309,8 @@ mod tests { #[test] fn test_mcp_tool_property_missing_type_defaults_to_string() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -2356,7 +2362,8 @@ mod tests { #[test] fn test_mcp_tool_integer_normalized_to_number() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -2404,7 +2411,8 @@ mod tests { #[test] fn test_mcp_tool_array_without_items_gets_default_string_items() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::ApplyPatchFreeform); @@ -2456,7 +2464,8 @@ mod tests { #[test] fn test_mcp_tool_anyof_defaults_to_string() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -2516,7 +2525,7 @@ mod tests { let expected = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"]. - + Examples of valid command strings: - ls -a (show hidden): ["powershell.exe", "-Command", "Get-ChildItem -Force"] @@ -2546,7 +2555,7 @@ Examples of valid command strings: let expected = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. - + Examples of valid command strings: - ls -a (show hidden): "Get-ChildItem -Force" @@ -2565,7 +2574,8 @@ Examples of valid command strings: #[test] fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index bbc7790c63..84785270f8 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -42,8 +42,7 @@ 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) { +pub(crate) fn set_deterministic_process_ids_for_tests(enabled: bool) { process_manager::set_deterministic_process_ids_for_tests(enabled); } diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 6740850092..251a4910ba 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -62,28 +62,20 @@ 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() } diff --git a/codex-rs/core/tests/common/Cargo.toml b/codex-rs/core/tests/common/Cargo.toml index 1fb43bb768..a7d35c0de7 100644 --- a/codex-rs/core/tests/common/Cargo.toml +++ b/codex-rs/core/tests/common/Cargo.toml @@ -11,7 +11,7 @@ path = "lib.rs" anyhow = { workspace = true } assert_cmd = { workspace = true } base64 = { workspace = true } -codex-core = { workspace = true, features = ["test-support"] } +codex-core = { workspace = true } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-cargo-bin = { workspace = true } diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 0144eeec5a..87a8e1adb2 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -20,7 +20,8 @@ pub mod test_codex_exec; #[ctor] fn enable_deterministic_unified_exec_process_ids_for_tests() { - codex_core::set_deterministic_process_ids_for_tests(true); + codex_core::test_support::set_thread_manager_test_mode(true); + codex_core::test_support::set_deterministic_process_ids(true); } #[track_caller] diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index af85ebb955..b06aa6c58e 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -170,7 +170,7 @@ impl TestCodexBuilder { resume_from: Option, ) -> anyhow::Result { let auth = self.auth.clone(); - let thread_manager = ThreadManager::with_models_provider_and_home( + let thread_manager = codex_core::test_support::thread_manager_with_models_provider_and_home( auth.clone(), config.model_provider.clone(), config.codex_home.clone(), @@ -179,7 +179,7 @@ impl TestCodexBuilder { let new_conversation = match resume_from { Some(path) => { - let auth_manager = codex_core::AuthManager::from_auth_for_testing(auth); + let auth_manager = codex_core::test_support::auth_manager_from_auth(auth); thread_manager .resume_thread_from_rollout(config.clone(), path, auth_manager) .await? diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 31703f7969..d22e5c66e4 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -1,7 +1,6 @@ use std::process::Command; use std::sync::Arc; -use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::ContentItem; use codex_core::ModelClient; @@ -10,7 +9,6 @@ use codex_core::Prompt; use codex_core::ResponseEvent; use codex_core::ResponseItem; use codex_core::WireApi; -use codex_core::models_manager::manager::ModelsManager; use codex_otel::OtelManager; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; @@ -65,14 +63,15 @@ async fn responses_stream_includes_subagent_header_on_review() { config.model_provider = provider.clone(); let effort = config.model_reasoning_effort; let summary = config.model_reasoning_summary; - let model = ModelsManager::get_model_offline(config.model.as_deref()); + let model = codex_core::test_support::get_model_offline(config.model.as_deref()); config.model = Some(model.clone()); let config = Arc::new(config); let conversation_id = ThreadId::new(); let auth_mode = TelemetryAuthMode::Chatgpt; let session_source = SessionSource::SubAgent(SubAgentSource::Review); - let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let model_info = + codex_core::test_support::construct_model_info_offline(model.as_str(), &config); let otel_manager = OtelManager::new( conversation_id, model.as_str(), @@ -169,14 +168,15 @@ async fn responses_stream_includes_subagent_header_on_other() { config.model_provider = provider.clone(); let effort = config.model_reasoning_effort; let summary = config.model_reasoning_summary; - let model = ModelsManager::get_model_offline(config.model.as_deref()); + let model = codex_core::test_support::get_model_offline(config.model.as_deref()); config.model = Some(model.clone()); let config = Arc::new(config); let conversation_id = ThreadId::new(); let auth_mode = TelemetryAuthMode::Chatgpt; let session_source = SessionSource::SubAgent(SubAgentSource::Other("my-task".to_string())); - let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let model_info = + codex_core::test_support::construct_model_info_offline(model.as_str(), &config); let otel_manager = OtelManager::new( conversation_id, @@ -275,12 +275,14 @@ async fn responses_respects_model_info_overrides_from_config() { let config = Arc::new(config); let conversation_id = ThreadId::new(); - let auth_mode = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")) - .auth_mode() - .map(TelemetryAuthMode::from); + let auth_mode = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("Test API Key")) + .auth_mode() + .map(TelemetryAuthMode::from); let session_source = SessionSource::SubAgent(SubAgentSource::Other("override-check".to_string())); - let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let model_info = + codex_core::test_support::construct_model_info_offline(model.as_str(), &config); let otel_manager = OtelManager::new( conversation_id, model.as_str(), diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 62d757e16e..b7b161aa86 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1,4 +1,3 @@ -use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::ContentItem; use codex_core::LocalShellAction; @@ -17,7 +16,6 @@ use codex_core::built_in_model_providers; use codex_core::default_client::originator; use codex_core::error::CodexErr; use codex_core::features::Feature; -use codex_core::models_manager::manager::ModelsManager; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SessionSource; @@ -573,7 +571,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { let auth_manager = match CodexAuth::from_auth_storage(codex_home.path(), AuthCredentialsStoreMode::File) { - Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth), + Ok(Some(auth)) => codex_core::test_support::auth_manager_from_auth(auth), Ok(None) => panic!("No CodexAuth found in codex_home"), Err(e) => panic!("Failed to load CodexAuth: {e}"), }; @@ -1318,12 +1316,14 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { config.model_provider = provider.clone(); let effort = config.model_reasoning_effort; let summary = config.model_reasoning_summary; - let model = ModelsManager::get_model_offline(config.model.as_deref()); + let model = codex_core::test_support::get_model_offline(config.model.as_deref()); config.model = Some(model.clone()); let config = Arc::new(config); - let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let model_info = + codex_core::test_support::construct_model_info_offline(model.as_str(), &config); let conversation_id = ThreadId::new(); - let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + let auth_manager = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("Test API Key")); let otel_manager = OtelManager::new( conversation_id, model.as_str(), diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index e5ef5c17a4..34ed529636 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -1,5 +1,4 @@ #![allow(clippy::expect_used, clippy::unwrap_used)] -use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::ContentItem; use codex_core::ModelClient; @@ -11,7 +10,6 @@ use codex_core::ResponseItem; use codex_core::WireApi; use codex_core::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER; use codex_core::features::Feature; -use codex_core::models_manager::manager::ModelsManager; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SessionSource; @@ -979,10 +977,11 @@ async fn websocket_harness_with_options( config.features.enable(Feature::ResponsesWebsocketsV2); } let config = Arc::new(config); - let mut model_info = ModelsManager::construct_model_info_offline(MODEL, &config); + let mut model_info = codex_core::test_support::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 auth_manager = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("Test API Key")); let exporter = InMemoryMetricExporter::default(); let metrics = MetricsClient::new( MetricsConfig::in_memory("test", "codex-core", env!("CARGO_PKG_VERSION"), exporter) diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index d8757fb388..325ad48296 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -1001,7 +1001,7 @@ async fn resume_conversation( config: &Config, path: std::path::PathBuf, ) -> Arc { - let auth_manager = codex_core::AuthManager::from_auth_for_testing( + let auth_manager = codex_core::test_support::auth_manager_from_auth( codex_core::CodexAuth::from_api_key("dummy"), ); manager diff --git a/codex-rs/core/tests/suite/model_info_overrides.rs b/codex-rs/core/tests/suite/model_info_overrides.rs index 5659454104..a9e18f9605 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -11,7 +11,7 @@ async fn offline_model_info_without_tool_output_override() { let codex_home = TempDir::new().expect("create temp dir"); let mut config = load_default_config_for_test(&codex_home).await; config.features.enable(Feature::RemoteModels); - let auth_manager = codex_core::AuthManager::from_auth_for_testing( + let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); let manager = ModelsManager::new(config.codex_home.clone(), auth_manager); @@ -30,7 +30,7 @@ async fn offline_model_info_with_tool_output_override() { let mut config = load_default_config_for_test(&codex_home).await; config.features.enable(Feature::RemoteModels); config.tool_output_token_limit = Some(123); - let auth_manager = codex_core::AuthManager::from_auth_for_testing( + let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); let manager = ModelsManager::new(config.codex_home.clone(), auth_manager); diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 5673e317f6..d0f4ecc561 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -47,7 +47,7 @@ async fn personality_does_not_mutate_base_instructions_without_template() { config.features.enable(Feature::Personality); config.personality = Some(Personality::Friendly); - let model_info = ModelsManager::construct_model_info_offline("gpt-5.1", &config); + let model_info = codex_core::test_support::construct_model_info_offline("gpt-5.1", &config); assert_eq!( model_info.get_model_instructions(config.personality), model_info.base_instructions @@ -62,7 +62,8 @@ async fn base_instructions_override_disables_personality_template() { config.personality = Some(Personality::Friendly); config.base_instructions = Some("override instructions".to_string()); - let model_info = ModelsManager::construct_model_info_offline("gpt-5.2-codex", &config); + let model_info = + codex_core::test_support::construct_model_info_offline("gpt-5.2-codex", &config); assert_eq!(model_info.base_instructions, "override instructions"); assert_eq!( @@ -470,7 +471,8 @@ async fn instructions_uses_base_if_feature_disabled() -> anyhow::Result<()> { config.features.disable(Feature::Personality); config.personality = Some(Personality::Friendly); - let model_info = ModelsManager::construct_model_info_offline("gpt-5.2-codex", &config); + let model_info = + codex_core::test_support::construct_model_info_offline("gpt-5.2-codex", &config); assert_eq!( model_info.get_model_instructions(config.personality), model_info.base_instructions diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 0af662f07b..d741cc26c1 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -100,9 +100,9 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<( base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let manager = ModelsManager::with_provider( + let manager = codex_core::test_support::models_manager_with_provider( codex_home.path().to_path_buf(), - codex_core::auth::AuthManager::from_auth_for_testing(auth), + codex_core::test_support::auth_manager_from_auth(auth), provider, ); @@ -497,9 +497,9 @@ async fn remote_models_preserve_builtin_presets() -> Result<()> { base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let manager = ModelsManager::with_provider( + let manager = codex_core::test_support::models_manager_with_provider( codex_home.path().to_path_buf(), - codex_core::auth::AuthManager::from_auth_for_testing(auth), + codex_core::test_support::auth_manager_from_auth(auth), provider, ); @@ -562,9 +562,9 @@ async fn remote_models_merge_adds_new_high_priority_first() -> Result<()> { base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let manager = ModelsManager::with_provider( + let manager = codex_core::test_support::models_manager_with_provider( codex_home.path().to_path_buf(), - codex_core::auth::AuthManager::from_auth_for_testing(auth), + codex_core::test_support::auth_manager_from_auth(auth), provider, ); @@ -613,9 +613,9 @@ async fn remote_models_merge_replaces_overlapping_model() -> Result<()> { base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let manager = ModelsManager::with_provider( + let manager = codex_core::test_support::models_manager_with_provider( codex_home.path().to_path_buf(), - codex_core::auth::AuthManager::from_auth_for_testing(auth), + codex_core::test_support::auth_manager_from_auth(auth), provider, ); @@ -661,9 +661,9 @@ async fn remote_models_merge_preserves_bundled_models_on_empty_response() -> Res base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let manager = ModelsManager::with_provider( + let manager = codex_core::test_support::models_manager_with_provider( codex_home.path().to_path_buf(), - codex_core::auth::AuthManager::from_auth_for_testing(auth), + codex_core::test_support::auth_manager_from_auth(auth), provider, ); @@ -706,9 +706,9 @@ async fn remote_models_request_times_out_after_5s() -> Result<()> { base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let manager = ModelsManager::with_provider( + let manager = codex_core::test_support::models_manager_with_provider( codex_home.path().to_path_buf(), - codex_core::auth::AuthManager::from_auth_for_testing(auth), + codex_core::test_support::auth_manager_from_auth(auth), provider, ); @@ -773,9 +773,9 @@ async fn remote_models_hide_picker_only_models() -> Result<()> { base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let manager = ModelsManager::with_provider( + let manager = codex_core::test_support::models_manager_with_provider( codex_home.path().to_path_buf(), - codex_core::auth::AuthManager::from_auth_for_testing(auth), + codex_core::test_support::auth_manager_from_auth(auth), provider, ); diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index a91013bedb..5bea0e1f0c 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -1,9 +1,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] -use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::NewThread; -use codex_core::ThreadManager; use codex_core::protocol::EventMsg; use codex_core::protocol::InitialHistory; use codex_core::protocol::ResumedHistory; @@ -58,11 +56,12 @@ async fn emits_warning_when_resumed_model_differs() { let initial_history = resume_history(&config, "previous-model", &rollout_path); - let thread_manager = ThreadManager::with_models_provider( + let thread_manager = codex_core::test_support::thread_manager_with_models_provider( CodexAuth::from_api_key("test"), config.model_provider.clone(), ); - let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); + let auth_manager = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); // Act: resume the conversation. let NewThread { diff --git a/codex-rs/core/tests/suite/unstable_features_warning.rs b/codex-rs/core/tests/suite/unstable_features_warning.rs index f148edc497..e82572abc0 100644 --- a/codex-rs/core/tests/suite/unstable_features_warning.rs +++ b/codex-rs/core/tests/suite/unstable_features_warning.rs @@ -1,9 +1,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] -use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::NewThread; -use codex_core::ThreadManager; use codex_core::config::CONFIG_TOML_FILE; use codex_core::features::Feature; use codex_core::protocol::EventMsg; @@ -30,11 +28,12 @@ async fn emits_warning_when_unstable_features_enabled_via_config() { toml! { features = { child_agents_md = true } }.into(), ); - let thread_manager = ThreadManager::with_models_provider( + let thread_manager = codex_core::test_support::thread_manager_with_models_provider( CodexAuth::from_api_key("test"), config.model_provider.clone(), ); - let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); + let auth_manager = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let NewThread { thread: conversation, @@ -67,11 +66,12 @@ async fn suppresses_warning_when_configured() { toml! { features = { child_agents_md = true } }.into(), ); - let thread_manager = ThreadManager::with_models_provider( + let thread_manager = codex_core::test_support::thread_manager_with_models_provider( CodexAuth::from_api_key("test"), config.model_provider.clone(), ); - let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); + let auth_manager = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let NewThread { thread: conversation, diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 1dced39b76..5e313f19b1 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -120,7 +120,7 @@ arboard = { workspace = true } [dev-dependencies] codex-cli = { workspace = true } -codex-core = { workspace = true, features = ["test-support"] } +codex-core = { workspace = true } codex-utils-cargo-bin = { workspace = true } codex-utils-pty = { workspace = true } assert_matches = { workspace = true } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index d30b747ebc..3d9b690886 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -2624,12 +2624,9 @@ mod tests { use crate::history_cell::HistoryCell; use crate::history_cell::UserHistoryCell; use crate::history_cell::new_session_info; - use codex_core::AuthManager; use codex_core::CodexAuth; - use codex_core::ThreadManager; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; - use codex_core::models_manager::manager::ModelsManager; use codex_core::protocol::AskForApproval; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; @@ -2726,14 +2723,17 @@ mod tests { async fn make_test_app() -> App { let (chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender().await; let config = chat_widget.config_ref().clone(); - let server = Arc::new(ThreadManager::with_models_provider( + let server = Arc::new( + codex_core::test_support::thread_manager_with_models_provider( + CodexAuth::from_api_key("Test API Key"), + config.model_provider.clone(), + ), + ); + let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::from_api_key("Test API Key"), - config.model_provider.clone(), - )); - let auth_manager = - AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + ); let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); - let model = ModelsManager::get_model_offline(config.model.as_deref()); + let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let otel_manager = test_otel_manager(&config, model.as_str()); App { @@ -2779,14 +2779,17 @@ mod tests { ) { let (chat_widget, app_event_tx, rx, op_rx) = make_chatwidget_manual_with_sender().await; let config = chat_widget.config_ref().clone(); - let server = Arc::new(ThreadManager::with_models_provider( + let server = Arc::new( + codex_core::test_support::thread_manager_with_models_provider( + CodexAuth::from_api_key("Test API Key"), + config.model_provider.clone(), + ), + ); + let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::from_api_key("Test API Key"), - config.model_provider.clone(), - )); - let auth_manager = - AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); + ); let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); - let model = ModelsManager::get_model_offline(config.model.as_deref()); + let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let otel_manager = test_otel_manager(&config, model.as_str()); ( @@ -2830,7 +2833,7 @@ mod tests { } fn test_otel_manager(config: &Config, model: &str) -> OtelManager { - let model_info = ModelsManager::construct_model_info_offline(model, config); + let model_info = codex_core::test_support::construct_model_info_offline(model, config); OtelManager::new( ThreadId::new(), model, @@ -2846,7 +2849,7 @@ mod tests { } fn all_model_presets() -> Vec { - codex_core::models_manager::model_presets::all_model_presets().clone() + codex_core::test_support::all_model_presets().clone() } fn model_migration_copy_to_plain_text( diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 51cc2b61ea..92fcf4ec53 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -16,7 +16,6 @@ use crate::test_backend::VT100Backend; use crate::tui::FrameRequester; use assert_matches::assert_matches; use codex_common::approval_presets::builtin_approval_presets; -use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::config::Config; use codex_core::config::ConfigBuilder; @@ -965,13 +964,16 @@ async fn helpers_are_available_and_do_not_panic() { let (tx_raw, _rx) = unbounded_channel::(); let tx = AppEventSender::new(tx_raw); let cfg = test_config().await; - let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref()); + let resolved_model = codex_core::test_support::get_model_offline(cfg.model.as_deref()); let otel_manager = test_otel_manager(&cfg, resolved_model.as_str()); - let thread_manager = Arc::new(ThreadManager::with_models_provider( - CodexAuth::from_api_key("test"), - cfg.model_provider.clone(), - )); - let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); + let thread_manager = Arc::new( + codex_core::test_support::thread_manager_with_models_provider( + CodexAuth::from_api_key("test"), + cfg.model_provider.clone(), + ), + ); + let auth_manager = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let init = ChatWidgetInit { config: cfg, frame_requester: FrameRequester::test_dummy(), @@ -993,7 +995,7 @@ async fn helpers_are_available_and_do_not_panic() { } fn test_otel_manager(config: &Config, model: &str) -> OtelManager { - let model_info = ModelsManager::construct_model_info_offline(model, config); + let model_info = codex_core::test_support::construct_model_info_offline(model, config); OtelManager::new( ThreadId::new(), model, @@ -1022,7 +1024,7 @@ async fn make_chatwidget_manual( let mut cfg = test_config().await; let resolved_model = model_override .map(str::to_owned) - .unwrap_or_else(|| ModelsManager::get_model_offline(cfg.model.as_deref())); + .unwrap_or_else(|| codex_core::test_support::get_model_offline(cfg.model.as_deref())); if let Some(model) = model_override { cfg.model = Some(model.to_string()); } @@ -1039,7 +1041,8 @@ async fn make_chatwidget_manual( }); bottom.set_steer_enabled(true); bottom.set_collaboration_modes_enabled(cfg.features.enabled(Feature::CollaborationModes)); - let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); + let auth_manager = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let codex_home = cfg.codex_home.clone(); let models_manager = Arc::new(ModelsManager::new(codex_home, auth_manager.clone())); let reasoning_effort = None; @@ -1144,8 +1147,9 @@ fn next_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) -> Op { } fn set_chatgpt_auth(chat: &mut ChatWidget) { - chat.auth_manager = - AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); + chat.auth_manager = codex_core::test_support::auth_manager_from_auth( + CodexAuth::create_dummy_chatgpt_auth_for_testing(), + ); chat.models_manager = Arc::new(ModelsManager::new( chat.config.codex_home.clone(), chat.auth_manager.clone(), @@ -1448,8 +1452,9 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() { #[tokio::test] async fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() { let (mut chat, _, _) = make_chatwidget_manual(Some(NUDGE_MODEL_SLUG)).await; - chat.auth_manager = - AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); + chat.auth_manager = codex_core::test_support::auth_manager_from_auth( + CodexAuth::create_dummy_chatgpt_auth_for_testing(), + ); chat.on_rate_limit_snapshot(Some(snapshot(95.0))); @@ -1463,7 +1468,7 @@ async fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() { async fn rate_limit_switch_prompt_skips_non_codex_limit() { let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = AuthManager::from_auth_for_testing(auth); + chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth); chat.on_rate_limit_snapshot(Some(RateLimitSnapshot { limit_id: Some("codex_other".to_string()), @@ -1488,7 +1493,7 @@ async fn rate_limit_switch_prompt_skips_non_codex_limit() { async fn rate_limit_switch_prompt_shows_once_per_session() { let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = AuthManager::from_auth_for_testing(auth); + chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth); chat.on_rate_limit_snapshot(Some(snapshot(90.0))); assert!( @@ -1512,7 +1517,7 @@ async fn rate_limit_switch_prompt_shows_once_per_session() { async fn rate_limit_switch_prompt_respects_hidden_notice() { let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = AuthManager::from_auth_for_testing(auth); + chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth); chat.config.notices.hide_rate_limit_model_nudge = Some(true); chat.on_rate_limit_snapshot(Some(snapshot(95.0))); @@ -1527,7 +1532,7 @@ async fn rate_limit_switch_prompt_respects_hidden_notice() { async fn rate_limit_switch_prompt_defers_until_task_complete() { let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = AuthManager::from_auth_for_testing(auth); + chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth); chat.bottom_pane.set_task_running(true); chat.on_rate_limit_snapshot(Some(snapshot(90.0))); @@ -1547,8 +1552,9 @@ async fn rate_limit_switch_prompt_defers_until_task_complete() { #[tokio::test] async fn rate_limit_switch_prompt_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = - AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); + chat.auth_manager = codex_core::test_support::auth_manager_from_auth( + CodexAuth::create_dummy_chatgpt_auth_for_testing(), + ); chat.on_rate_limit_snapshot(Some(snapshot(92.0))); chat.maybe_show_pending_rate_limit_prompt(); @@ -1881,8 +1887,9 @@ async fn plan_implementation_popup_shows_after_proposed_plan_output() { #[tokio::test] async fn plan_implementation_popup_skips_when_rate_limit_prompt_pending() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; - chat.auth_manager = - AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); + chat.auth_manager = codex_core::test_support::auth_manager_from_auth( + CodexAuth::create_dummy_chatgpt_auth_for_testing(), + ); chat.set_feature_enabled(Feature::CollaborationModes, true); let plan_mask = collaboration_modes::mask_for_kind(chat.models_manager.as_ref(), ModeKind::Plan) @@ -3143,13 +3150,16 @@ async fn collaboration_modes_defaults_to_code_on_startup() { .build() .await .expect("config"); - let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref()); + let resolved_model = codex_core::test_support::get_model_offline(cfg.model.as_deref()); let otel_manager = test_otel_manager(&cfg, resolved_model.as_str()); - let thread_manager = Arc::new(ThreadManager::with_models_provider( - CodexAuth::from_api_key("test"), - cfg.model_provider.clone(), - )); - let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); + let thread_manager = Arc::new( + codex_core::test_support::thread_manager_with_models_provider( + CodexAuth::from_api_key("test"), + cfg.model_provider.clone(), + ), + ); + let auth_manager = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let init = ChatWidgetInit { config: cfg, frame_requester: FrameRequester::test_dummy(), @@ -3189,13 +3199,16 @@ async fn experimental_mode_plan_applies_on_startup() { .build() .await .expect("config"); - let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref()); + let resolved_model = codex_core::test_support::get_model_offline(cfg.model.as_deref()); let otel_manager = test_otel_manager(&cfg, resolved_model.as_str()); - let thread_manager = Arc::new(ThreadManager::with_models_provider( - CodexAuth::from_api_key("test"), - cfg.model_provider.clone(), - )); - let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); + let thread_manager = Arc::new( + codex_core::test_support::thread_manager_with_models_provider( + CodexAuth::from_api_key("test"), + cfg.model_provider.clone(), + ), + ); + let auth_manager = + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let init = ChatWidgetInit { config: cfg, frame_requester: FrameRequester::test_dummy(), diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index bff2fab048..37926009a8 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -7,7 +7,6 @@ use chrono::Utc; use codex_core::AuthManager; use codex_core::config::Config; use codex_core::config::ConfigBuilder; -use codex_core::models_manager::manager::ModelsManager; use codex_core::protocol::CreditsSnapshot; use codex_core::protocol::RateLimitSnapshot; use codex_core::protocol::RateLimitWindow; @@ -40,7 +39,7 @@ fn test_auth_manager(config: &Config) -> AuthManager { fn token_info_for(model_slug: &str, config: &Config, usage: &TokenUsage) -> TokenUsageInfo { let context_window = - ModelsManager::construct_model_info_offline(model_slug, config).context_window; + codex_core::test_support::construct_model_info_offline(model_slug, config).context_window; TokenUsageInfo { total_token_usage: usage.clone(), last_token_usage: usage.clone(), @@ -139,7 +138,7 @@ async fn status_snapshot_includes_reasoning_details() { }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let reasoning_effort_override = Some(Some(ReasoningEffort::High)); @@ -190,7 +189,7 @@ async fn status_snapshot_includes_forked_from() { .single() .expect("valid time"); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let session_id = ThreadId::from_string("0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e").expect("session id"); @@ -257,7 +256,7 @@ async fn status_snapshot_includes_monthly_limit() { }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -307,7 +306,7 @@ async fn status_snapshot_shows_unlimited_credits() { plan_type: None, }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -356,7 +355,7 @@ async fn status_snapshot_shows_positive_credits() { plan_type: None, }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -405,7 +404,7 @@ async fn status_snapshot_hides_zero_credits() { plan_type: None, }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -452,7 +451,7 @@ async fn status_snapshot_hides_when_has_no_credits_flag() { plan_type: None, }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -497,7 +496,7 @@ async fn status_card_token_usage_excludes_cached_tokens() { .single() .expect("timestamp"); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -558,7 +557,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() { }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let reasoning_effort_override = Some(Some(ReasoningEffort::High)); let composite = new_status_output( @@ -608,7 +607,7 @@ async fn status_snapshot_shows_missing_limits_message() { .single() .expect("timestamp"); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -677,7 +676,7 @@ async fn status_snapshot_includes_credits_and_limits() { }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -734,7 +733,7 @@ async fn status_snapshot_shows_empty_limits_message() { .expect("timestamp"); let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -800,7 +799,7 @@ async fn status_snapshot_shows_stale_limits_message() { let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); let now = captured_at + ChronoDuration::minutes(20); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -870,7 +869,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() { let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); let now = captured_at + ChronoDuration::minutes(20); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = token_info_for(&model_slug, &config, &usage); let composite = new_status_output( &config, @@ -924,7 +923,7 @@ async fn status_context_window_uses_last_usage() { .single() .expect("timestamp"); - let model_slug = ModelsManager::get_model_offline(config.model.as_deref()); + let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref()); let token_info = TokenUsageInfo { total_token_usage: total_usage.clone(), last_token_usage: last_usage,