diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index 8b44481060..e784498c2a 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -636,8 +636,8 @@ mod tests { use crate::auth::storage::FileAuthStorage; use crate::auth::storage::get_auth_file; use crate::config::Config; - use crate::config::ConfigOverrides; - use crate::config::ConfigToml; + use crate::config::ConfigBuilder; + use crate::config_loader::LoaderOverrides; use crate::token_data::IdTokenInfo; use crate::token_data::KnownPlan as InternalKnownPlan; use crate::token_data::PlanType as InternalPlanType; @@ -862,22 +862,30 @@ mod tests { Ok(fake_jwt) } - fn build_config( + async fn build_config( codex_home: &Path, forced_login_method: Option, forced_chatgpt_workspace_id: Option, ) -> Config { - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.to_path_buf(), - ) - .expect("config should load"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home)) + .build() + .await + .expect("config should load"); config.forced_login_method = forced_login_method; config.forced_chatgpt_workspace_id = forced_chatgpt_workspace_id; config } + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } + } + /// Use sparingly. /// TODO (gpeal): replace this with an injectable env var provider. #[cfg(test)] @@ -915,7 +923,7 @@ mod tests { login_with_api_key(codex_home.path(), "sk-test", AuthCredentialsStoreMode::File) .expect("seed api key"); - let config = build_config(codex_home.path(), Some(ForcedLoginMethod::Chatgpt), None); + let config = build_config(codex_home.path(), Some(ForcedLoginMethod::Chatgpt), None).await; let err = super::enforce_login_restrictions(&config) .await @@ -941,7 +949,7 @@ mod tests { ) .expect("failed to write auth file"); - let config = build_config(codex_home.path(), None, Some("org_mine".to_string())); + let config = build_config(codex_home.path(), None, Some("org_mine".to_string())).await; let err = super::enforce_login_restrictions(&config) .await @@ -967,7 +975,7 @@ mod tests { ) .expect("failed to write auth file"); - let config = build_config(codex_home.path(), None, Some("org_mine".to_string())); + let config = build_config(codex_home.path(), None, Some("org_mine".to_string())).await; super::enforce_login_restrictions(&config) .await @@ -985,7 +993,7 @@ mod tests { login_with_api_key(codex_home.path(), "sk-test", AuthCredentialsStoreMode::File) .expect("seed api key"); - let config = build_config(codex_home.path(), None, Some("org_mine".to_string())); + let config = build_config(codex_home.path(), None, Some("org_mine".to_string())).await; super::enforce_login_restrictions(&config) .await @@ -1002,7 +1010,7 @@ mod tests { let _guard = EnvVarGuard::set(CODEX_API_KEY_ENV_VAR, "sk-env"); let codex_home = tempdir().unwrap(); - let config = build_config(codex_home.path(), Some(ForcedLoginMethod::Chatgpt), None); + let config = build_config(codex_home.path(), Some(ForcedLoginMethod::Chatgpt), None).await; let err = super::enforce_login_restrictions(&config) .await diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c15fa03cfd..dd3e468719 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2749,8 +2749,8 @@ pub(crate) use tests::make_session_and_context_with_rx; mod tests { use super::*; use crate::CodexAuth; - use crate::config::ConfigOverrides; - use crate::config::ConfigToml; + use crate::config::ConfigBuilder; + use crate::config_loader::LoaderOverrides; use crate::exec::ExecToolCallOutput; use crate::function_tool::FunctionCallError; use crate::shell::default_user_shell; @@ -2777,6 +2777,7 @@ mod tests { use codex_app_server_protocol::AuthMode; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; + use std::path::Path; use std::time::Duration; use tokio::time::sleep; @@ -2789,9 +2790,9 @@ mod tests { use std::sync::Arc; use std::time::Duration as StdDuration; - #[test] - fn reconstruct_history_matches_live_compactions() { - let (session, turn_context) = make_session_and_context(); + #[tokio::test] + async fn reconstruct_history_matches_live_compactions() { + let (session, turn_context) = make_session_and_context().await; let (rollout_items, expected) = sample_rollout(&session, &turn_context); let reconstructed = session.reconstruct_history_from_rollout(&turn_context, &rollout_items); @@ -2799,47 +2800,40 @@ mod tests { assert_eq!(expected, reconstructed); } - #[test] - fn record_initial_history_reconstructs_resumed_transcript() { - let (session, turn_context) = make_session_and_context(); + #[tokio::test] + async fn record_initial_history_reconstructs_resumed_transcript() { + let (session, turn_context) = make_session_and_context().await; let (rollout_items, expected) = sample_rollout(&session, &turn_context); - tokio_test::block_on(session.record_initial_history(InitialHistory::Resumed( - ResumedHistory { + session + .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ConversationId::default(), history: rollout_items, rollout_path: PathBuf::from("/tmp/resume.jsonl"), - }, - ))); + })) + .await; - let actual = tokio_test::block_on(async { - session.state.lock().await.clone_history().get_history() - }); + let actual = session.state.lock().await.clone_history().get_history(); assert_eq!(expected, actual); } - #[test] - fn record_initial_history_reconstructs_forked_transcript() { - let (session, turn_context) = make_session_and_context(); + #[tokio::test] + async fn record_initial_history_reconstructs_forked_transcript() { + let (session, turn_context) = make_session_and_context().await; let (rollout_items, expected) = sample_rollout(&session, &turn_context); - tokio_test::block_on(session.record_initial_history(InitialHistory::Forked(rollout_items))); + session + .record_initial_history(InitialHistory::Forked(rollout_items)) + .await; - let actual = tokio_test::block_on(async { - session.state.lock().await.clone_history().get_history() - }); + let actual = session.state.lock().await.clone_history().get_history(); assert_eq!(expected, actual); } - #[test] - fn set_rate_limits_retains_previous_credits() { + #[tokio::test] + async fn set_rate_limits_retains_previous_credits() { let codex_home = tempfile::tempdir().expect("create temp dir"); - let config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load default test config"); + 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 session_configuration = SessionConfiguration { @@ -2903,15 +2897,10 @@ mod tests { ); } - #[test] - fn set_rate_limits_updates_plan_type_when_present() { + #[tokio::test] + async fn set_rate_limits_updates_plan_type_when_present() { let codex_home = tempfile::tempdir().expect("create temp dir"); - let config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load default test config"); + 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 session_configuration = SessionConfiguration { @@ -3001,8 +2990,8 @@ mod tests { assert_eq!(expected, got); } - #[test] - fn includes_timed_out_message() { + #[tokio::test] + async fn includes_timed_out_message() { let exec = ExecToolCallOutput { exit_code: 0, stdout: StreamOutput::new(String::new()), @@ -3011,7 +3000,7 @@ mod tests { duration: StdDuration::from_secs(1), timed_out: true, }; - let (_, turn_context) = make_session_and_context(); + let (_, turn_context) = make_session_and_context().await; let out = format_exec_output_str(&exec, turn_context.truncation_policy); @@ -3084,6 +3073,23 @@ mod tests { }) } + async fn build_test_config(codex_home: &Path) -> Config { + ConfigBuilder::default() + .codex_home(codex_home.to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home)) + .build() + .await + .expect("load default test config") + } + + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } + } + fn otel_manager( conversation_id: ConversationId, config: &Config, @@ -3103,15 +3109,10 @@ mod tests { ) } - pub(crate) fn make_session_and_context() -> (Session, TurnContext) { + pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let (tx_event, _rx_event) = async_channel::unbounded(); let codex_home = tempfile::tempdir().expect("create temp dir"); - let config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load default test config"); + let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); let conversation_id = ConversationId::default(); let auth_manager = @@ -3190,19 +3191,14 @@ mod tests { // Like make_session_and_context, but returns Arc and the event receiver // so tests can assert on emitted events. - pub(crate) fn make_session_and_context_with_rx() -> ( + pub(crate) async fn make_session_and_context_with_rx() -> ( Arc, Arc, async_channel::Receiver, ) { let (tx_event, rx_event) = async_channel::unbounded(); let codex_home = tempfile::tempdir().expect("create temp dir"); - let config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load default test config"); + let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); let conversation_id = ConversationId::default(); let auth_manager = @@ -3281,7 +3277,7 @@ mod tests { #[tokio::test] async fn record_model_warning_appends_user_message() { - let (mut session, turn_context) = make_session_and_context(); + let (mut session, turn_context) = make_session_and_context().await; let mut features = Features::with_defaults(); features.enable(Feature::ModelWarnings); session.features = features; @@ -3340,7 +3336,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[test_log::test] async fn abort_regular_task_emits_turn_aborted_only() { - let (sess, tc, rx) = make_session_and_context_with_rx(); + let (sess, tc, rx) = make_session_and_context_with_rx().await; let input = vec![UserInput::Text { text: "hello".to_string(), }]; @@ -3369,7 +3365,7 @@ mod tests { #[tokio::test] async fn abort_gracefuly_emits_turn_aborted_only() { - let (sess, tc, rx) = make_session_and_context_with_rx(); + let (sess, tc, rx) = make_session_and_context_with_rx().await; let input = vec![UserInput::Text { text: "hello".to_string(), }]; @@ -3395,7 +3391,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn abort_review_task_emits_exited_then_aborted_and_records_history() { - let (sess, tc, rx) = make_session_and_context_with_rx(); + let (sess, tc, rx) = make_session_and_context_with_rx().await; let input = vec![UserInput::Text { text: "start review".to_string(), }]; @@ -3443,7 +3439,7 @@ mod tests { #[tokio::test] async fn fatal_tool_error_stops_turn_and_reports_error() { - let (session, turn_context, _rx) = make_session_and_context_with_rx(); + let (session, turn_context, _rx) = make_session_and_context_with_rx().await; let tools = { session .services @@ -3606,7 +3602,7 @@ mod tests { use crate::turn_diff_tracker::TurnDiffTracker; use std::collections::HashMap; - let (session, mut turn_context_raw) = make_session_and_context(); + let (session, mut turn_context_raw) = make_session_and_context().await; // Ensure policy is NOT OnRequest so the early rejection path triggers turn_context_raw.approval_policy = AskForApproval::OnFailure; let session = Arc::new(session); @@ -3737,7 +3733,7 @@ mod tests { use crate::sandboxing::SandboxPermissions; use crate::turn_diff_tracker::TurnDiffTracker; - let (session, mut turn_context_raw) = make_session_and_context(); + let (session, mut turn_context_raw) = make_session_and_context().await; turn_context_raw.approval_policy = AskForApproval::OnFailure; let session = Arc::new(session); let turn_context = Arc::new(turn_context_raw); diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index c7aebbaf92..240a267041 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -366,7 +366,7 @@ mod tests { rx_event: rx_events, }); - let (session, ctx, _rx_evt) = crate::codex::make_session_and_context_with_rx(); + let (session, ctx, _rx_evt) = crate::codex::make_session_and_context_with_rx().await; let (tx_out, rx_out) = bounded(1); tx_out diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 438e441b5c..c958bcabbe 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -992,14 +992,13 @@ pub fn resolve_oss_provider( } impl Config { - /// Meant to be used exclusively for tests. For new tests, prefer using - /// [ConfigBuilder::build()], if possible, so ultimately we can make this - /// method private to this file. - pub fn load_from_base_config_with_overrides( + #[cfg(test)] + fn load_from_base_config_with_overrides( cfg: ConfigToml, overrides: ConfigOverrides, codex_home: PathBuf, ) -> std::io::Result { + // Note this ignores requirements.toml enforcement for tests. let requirements = ConfigRequirements::default(); Self::load_config_with_requirements(cfg, overrides, codex_home, requirements) } diff --git a/codex-rs/core/src/conversation_manager.rs b/codex-rs/core/src/conversation_manager.rs index ce38b0018c..084b73886d 100644 --- a/codex-rs/core/src/conversation_manager.rs +++ b/codex-rs/core/src/conversation_manager.rs @@ -379,9 +379,9 @@ mod tests { assert_matches!(truncated2, InitialHistory::New); } - #[test] - fn ignores_session_prefix_messages_when_truncating() { - let (session, turn_context) = make_session_and_context(); + #[tokio::test] + async fn ignores_session_prefix_messages_when_truncating() { + let (session, turn_context) = make_session_and_context().await; let mut items = session.build_initial_context(&turn_context); items.push(user_msg("feature request")); items.push(assistant_msg("ack")); diff --git a/codex-rs/core/src/message_history.rs b/codex-rs/core/src/message_history.rs index ecc6851336..862a7afed9 100644 --- a/codex-rs/core/src/message_history.rs +++ b/codex-rs/core/src/message_history.rs @@ -401,13 +401,14 @@ fn history_log_id(_metadata: &std::fs::Metadata) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::config::Config; - use crate::config::ConfigOverrides; - use crate::config::ConfigToml; + + use crate::config::ConfigBuilder; + use crate::config_loader::LoaderOverrides; use codex_protocol::ConversationId; use pretty_assertions::assert_eq; use std::fs::File; use std::io::Write; + use std::path::Path; use tempfile::TempDir; #[tokio::test] @@ -493,12 +494,12 @@ mod tests { async fn append_entry_trims_history_when_beyond_max_bytes() { let codex_home = TempDir::new().expect("create temp dir"); - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load config"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home.path())) + .build() + .await + .expect("load config"); let conversation_id = ConversationId::new(); @@ -541,12 +542,12 @@ mod tests { async fn append_entry_trims_history_to_soft_cap() { let codex_home = TempDir::new().expect("create temp dir"); - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load config"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home.path())) + .build() + .await + .expect("load config"); let conversation_id = ConversationId::new(); @@ -615,4 +616,12 @@ mod tests { assert_eq!(pruned_len, long_entry_len); assert!(pruned_len <= soft_cap_bytes.max(long_entry_len)); } + + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } + } } diff --git a/codex-rs/core/src/openai_models/models_manager.rs b/codex-rs/core/src/openai_models/models_manager.rs index 9969a3a9c5..5271e1ff71 100644 --- a/codex-rs/core/src/openai_models/models_manager.rs +++ b/codex-rs/core/src/openai_models/models_manager.rs @@ -314,15 +314,16 @@ mod tests { use super::*; use crate::CodexAuth; use crate::auth::AuthCredentialsStoreMode; - use crate::config::Config; - use crate::config::ConfigOverrides; - use crate::config::ConfigToml; + + use crate::config::ConfigBuilder; + use crate::config_loader::LoaderOverrides; use crate::features::Feature; use crate::model_provider_info::WireApi; use codex_protocol::openai_models::ModelsResponse; use core_test_support::responses::mount_models_once; use pretty_assertions::assert_eq; use serde_json::json; + use std::path::Path; use tempfile::tempdir; use wiremock::MockServer; @@ -380,6 +381,14 @@ mod tests { } } + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } + } + #[tokio::test] async fn refresh_available_models_sorts_and_marks_default() { let server = MockServer::start().await; @@ -397,12 +406,12 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load default test config"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home.path())) + .build() + .await + .expect("load default test config"); config.features.enable(Feature::RemoteModels); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); @@ -455,12 +464,12 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load default test config"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home.path())) + .build() + .await + .expect("load default test config"); config.features.enable(Feature::RemoteModels); let auth_manager = Arc::new(AuthManager::new( codex_home.path().to_path_buf(), @@ -511,12 +520,12 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load default test config"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home.path())) + .build() + .await + .expect("load default test config"); config.features.enable(Feature::RemoteModels); let auth_manager = Arc::new(AuthManager::new( codex_home.path().to_path_buf(), @@ -587,12 +596,12 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("load default test config"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home.path())) + .build() + .await + .expect("load default test config"); config.features.enable(Feature::RemoteModels); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); diff --git a/codex-rs/core/src/project_doc.rs b/codex-rs/core/src/project_doc.rs index f115b1295c..a80862f520 100644 --- a/codex-rs/core/src/project_doc.rs +++ b/codex-rs/core/src/project_doc.rs @@ -232,10 +232,11 @@ fn merge_project_docs_with_skills( #[cfg(test)] mod tests { use super::*; - use crate::config::ConfigOverrides; - use crate::config::ConfigToml; + use crate::config::ConfigBuilder; + use crate::config_loader::LoaderOverrides; use crate::skills::load_skills; use std::fs; + use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; @@ -244,14 +245,14 @@ mod tests { /// optionally specify a custom `instructions` string – when `None` the /// value is cleared to mimic a scenario where no system instructions have /// been configured. - fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -> Config { + async fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -> Config { let codex_home = TempDir::new().unwrap(); - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("defaults for test should always succeed"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home.path())) + .build() + .await + .expect("defaults for test should always succeed"); config.cwd = root.path().to_path_buf(); config.project_doc_max_bytes = limit; @@ -260,13 +261,13 @@ mod tests { config } - fn make_config_with_fallback( + async fn make_config_with_fallback( root: &TempDir, limit: usize, instructions: Option<&str>, fallbacks: &[&str], ) -> Config { - let mut config = make_config(root, limit, instructions); + let mut config = make_config(root, limit, instructions).await; config.project_doc_fallback_filenames = fallbacks .iter() .map(std::string::ToString::to_string) @@ -279,7 +280,7 @@ mod tests { async fn no_doc_file_returns_none() { let tmp = tempfile::tempdir().expect("tempdir"); - let res = get_user_instructions(&make_config(&tmp, 4096, None), None).await; + let res = get_user_instructions(&make_config(&tmp, 4096, None).await, None).await; assert!( res.is_none(), "Expected None when AGENTS.md is absent and no system instructions provided" @@ -293,7 +294,7 @@ mod tests { let tmp = tempfile::tempdir().expect("tempdir"); fs::write(tmp.path().join("AGENTS.md"), "hello world").unwrap(); - let res = get_user_instructions(&make_config(&tmp, 4096, None), None) + let res = get_user_instructions(&make_config(&tmp, 4096, None).await, None) .await .expect("doc expected"); @@ -312,7 +313,7 @@ mod tests { let huge = "A".repeat(LIMIT * 2); // 2 KiB fs::write(tmp.path().join("AGENTS.md"), &huge).unwrap(); - let res = get_user_instructions(&make_config(&tmp, LIMIT, None), None) + let res = get_user_instructions(&make_config(&tmp, LIMIT, None).await, None) .await .expect("doc expected"); @@ -341,7 +342,7 @@ mod tests { std::fs::create_dir_all(&nested).unwrap(); // Build config pointing at the nested dir. - let mut cfg = make_config(&repo, 4096, None); + let mut cfg = make_config(&repo, 4096, None).await; cfg.cwd = nested; let res = get_user_instructions(&cfg, None) @@ -356,7 +357,7 @@ mod tests { let tmp = tempfile::tempdir().expect("tempdir"); fs::write(tmp.path().join("AGENTS.md"), "something").unwrap(); - let res = get_user_instructions(&make_config(&tmp, 0, None), None).await; + let res = get_user_instructions(&make_config(&tmp, 0, None).await, None).await; assert!( res.is_none(), "With limit 0 the function should return None" @@ -372,7 +373,7 @@ mod tests { const INSTRUCTIONS: &str = "base instructions"; - let res = get_user_instructions(&make_config(&tmp, 4096, Some(INSTRUCTIONS)), None) + let res = get_user_instructions(&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await, None) .await .expect("should produce a combined instruction string"); @@ -389,7 +390,8 @@ mod tests { const INSTRUCTIONS: &str = "some instructions"; - let res = get_user_instructions(&make_config(&tmp, 4096, Some(INSTRUCTIONS)), None).await; + let res = + get_user_instructions(&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await, None).await; assert_eq!(res, Some(INSTRUCTIONS.to_string())); } @@ -415,7 +417,7 @@ mod tests { std::fs::create_dir_all(&nested).unwrap(); fs::write(nested.join("AGENTS.md"), "crate doc").unwrap(); - let mut cfg = make_config(&repo, 4096, None); + let mut cfg = make_config(&repo, 4096, None).await; cfg.cwd = nested; let res = get_user_instructions(&cfg, None) @@ -431,7 +433,7 @@ mod tests { fs::write(tmp.path().join(DEFAULT_PROJECT_DOC_FILENAME), "versioned").unwrap(); fs::write(tmp.path().join(LOCAL_PROJECT_DOC_FILENAME), "local").unwrap(); - let cfg = make_config(&tmp, 4096, None); + let cfg = make_config(&tmp, 4096, None).await; let res = get_user_instructions(&cfg, None) .await @@ -453,7 +455,7 @@ mod tests { let tmp = tempfile::tempdir().expect("tempdir"); fs::write(tmp.path().join("EXAMPLE.md"), "example instructions").unwrap(); - let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md"]); + let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md"]).await; let res = get_user_instructions(&cfg, None) .await @@ -469,7 +471,8 @@ mod tests { fs::write(tmp.path().join("AGENTS.md"), "primary").unwrap(); fs::write(tmp.path().join("EXAMPLE.md"), "secondary").unwrap(); - let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md", ".example.md"]); + let cfg = + make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md", ".example.md"]).await; let res = get_user_instructions(&cfg, None) .await @@ -493,7 +496,7 @@ mod tests { let tmp = tempfile::tempdir().expect("tempdir"); fs::write(tmp.path().join("AGENTS.md"), "base doc").unwrap(); - let cfg = make_config(&tmp, 4096, None); + let cfg = make_config(&tmp, 4096, None).await; create_skill( cfg.codex_home.clone(), "pdf-processing", @@ -524,7 +527,7 @@ mod tests { #[tokio::test] async fn skills_render_without_project_doc() { let tmp = tempfile::tempdir().expect("tempdir"); - let cfg = make_config(&tmp, 4096, None); + let cfg = make_config(&tmp, 4096, None).await; create_skill(cfg.codex_home.clone(), "linting", "run clippy"); let skills = load_skills(&cfg); @@ -551,4 +554,13 @@ mod tests { let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n"); fs::write(skill_dir.join("SKILL.md"), content).unwrap(); } + + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } + } + } diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs index 32c5db8438..284796b325 100644 --- a/codex-rs/core/src/skills/loader.rs +++ b/codex-rs/core/src/skills/loader.rs @@ -279,21 +279,21 @@ fn extract_frontmatter(contents: &str) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::config::ConfigOverrides; - use crate::config::ConfigToml; + use crate::config::ConfigBuilder; + use crate::config_loader::LoaderOverrides; use codex_protocol::protocol::SkillScope; use pretty_assertions::assert_eq; use std::path::Path; use std::process::Command; use tempfile::TempDir; - fn make_config(codex_home: &TempDir) -> Config { - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ) - .expect("defaults for test should always succeed"); + async fn make_config(codex_home: &TempDir) -> Config { + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(codex_home.path())) + .build() + .await + .expect("defaults for test should always succeed"); config.cwd = codex_home.path().to_path_buf(); config @@ -329,11 +329,19 @@ mod tests { path } - #[test] - fn loads_valid_skill() { + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } + } + + #[tokio::test] + async fn loads_valid_skill() { let codex_home = tempfile::tempdir().expect("tempdir"); write_skill(&codex_home, "demo", "demo-skill", "does things\ncarefully"); - let cfg = make_config(&codex_home); + let cfg = make_config(&codex_home).await; let outcome = load_skills(&cfg); assert!( @@ -352,8 +360,8 @@ mod tests { ); } - #[test] - fn skips_hidden_and_invalid() { + #[tokio::test] + async fn skips_hidden_and_invalid() { let codex_home = tempfile::tempdir().expect("tempdir"); let hidden_dir = codex_home.path().join("skills/.hidden"); fs::create_dir_all(&hidden_dir).unwrap(); @@ -368,7 +376,7 @@ mod tests { fs::create_dir_all(&invalid_dir).unwrap(); fs::write(invalid_dir.join(SKILLS_FILENAME), "---\nname: bad").unwrap(); - let cfg = make_config(&codex_home); + let cfg = make_config(&codex_home).await; let outcome = load_skills(&cfg); assert_eq!(outcome.skills.len(), 0); assert_eq!(outcome.errors.len(), 1); @@ -380,12 +388,12 @@ mod tests { ); } - #[test] - fn enforces_length_limits() { + #[tokio::test] + async fn enforces_length_limits() { let codex_home = tempfile::tempdir().expect("tempdir"); let max_desc = "\u{1F4A1}".repeat(MAX_DESCRIPTION_LEN); write_skill(&codex_home, "max-len", "max-len", &max_desc); - let cfg = make_config(&codex_home); + let cfg = make_config(&codex_home).await; let outcome = load_skills(&cfg); assert!( @@ -406,8 +414,8 @@ mod tests { ); } - #[test] - fn loads_skills_from_repo_root() { + #[tokio::test] + async fn loads_skills_from_repo_root() { let codex_home = tempfile::tempdir().expect("tempdir"); let repo_dir = tempfile::tempdir().expect("tempdir"); @@ -423,7 +431,7 @@ mod tests { .join(REPO_ROOT_CONFIG_DIR_NAME) .join(SKILLS_DIR_NAME); write_skill_at(&skills_root, "repo", "repo-skill", "from repo"); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = repo_dir.path().to_path_buf(); let repo_root = normalize_path(&skills_root).unwrap_or_else(|_| skills_root.clone()); @@ -439,8 +447,8 @@ mod tests { assert!(skill.path.starts_with(&repo_root)); } - #[test] - fn loads_skills_from_nearest_codex_dir_under_repo_root() { + #[tokio::test] + async fn loads_skills_from_nearest_codex_dir_under_repo_root() { let codex_home = tempfile::tempdir().expect("tempdir"); let repo_dir = tempfile::tempdir().expect("tempdir"); @@ -474,7 +482,7 @@ mod tests { "from nested", ); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = nested_dir; let outcome = load_skills(&cfg); @@ -487,8 +495,8 @@ mod tests { assert_eq!(outcome.skills[0].name, "nested-skill"); } - #[test] - fn loads_skills_from_codex_dir_when_not_git_repo() { + #[tokio::test] + async fn loads_skills_from_codex_dir_when_not_git_repo() { let codex_home = tempfile::tempdir().expect("tempdir"); let work_dir = tempfile::tempdir().expect("tempdir"); @@ -502,7 +510,7 @@ mod tests { "from cwd", ); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = work_dir.path().to_path_buf(); let outcome = load_skills(&cfg); @@ -516,8 +524,8 @@ mod tests { assert_eq!(outcome.skills[0].scope, SkillScope::Repo); } - #[test] - fn deduplicates_by_name_preferring_repo_over_user() { + #[tokio::test] + async fn deduplicates_by_name_preferring_repo_over_user() { let codex_home = tempfile::tempdir().expect("tempdir"); let repo_dir = tempfile::tempdir().expect("tempdir"); @@ -539,7 +547,7 @@ mod tests { "from repo", ); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = repo_dir.path().to_path_buf(); let outcome = load_skills(&cfg); @@ -553,14 +561,14 @@ mod tests { assert_eq!(outcome.skills[0].scope, SkillScope::Repo); } - #[test] - fn loads_system_skills_with_lowest_priority() { + #[tokio::test] + async fn loads_system_skills_with_lowest_priority() { let codex_home = tempfile::tempdir().expect("tempdir"); write_system_skill(&codex_home, "system", "dupe-skill", "from system"); write_skill(&codex_home, "user", "dupe-skill", "from user"); - let cfg = make_config(&codex_home); + let cfg = make_config(&codex_home).await; let outcome = load_skills(&cfg); assert!( outcome.errors.is_empty(), @@ -572,8 +580,8 @@ mod tests { assert_eq!(outcome.skills[0].scope, SkillScope::User); } - #[test] - fn repo_skills_search_does_not_escape_repo_root() { + #[tokio::test] + async fn repo_skills_search_does_not_escape_repo_root() { let codex_home = tempfile::tempdir().expect("tempdir"); let outer_dir = tempfile::tempdir().expect("tempdir"); let repo_dir = outer_dir.path().join("repo"); @@ -596,7 +604,7 @@ mod tests { .expect("git init"); assert!(status.success(), "git init failed"); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = repo_dir; let outcome = load_skills(&cfg); @@ -608,8 +616,8 @@ mod tests { assert_eq!(outcome.skills.len(), 0); } - #[test] - fn loads_skills_when_cwd_is_file_in_repo() { + #[tokio::test] + async fn loads_skills_when_cwd_is_file_in_repo() { let codex_home = tempfile::tempdir().expect("tempdir"); let repo_dir = tempfile::tempdir().expect("tempdir"); @@ -632,7 +640,7 @@ mod tests { let file_path = repo_dir.path().join("some-file.txt"); fs::write(&file_path, "contents").unwrap(); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = file_path; let outcome = load_skills(&cfg); @@ -646,8 +654,8 @@ mod tests { assert_eq!(outcome.skills[0].scope, SkillScope::Repo); } - #[test] - fn non_git_repo_skills_search_does_not_walk_parents() { + #[tokio::test] + async fn non_git_repo_skills_search_does_not_walk_parents() { let codex_home = tempfile::tempdir().expect("tempdir"); let outer_dir = tempfile::tempdir().expect("tempdir"); let nested_dir = outer_dir.path().join("nested/inner"); @@ -663,7 +671,7 @@ mod tests { "from outer", ); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = nested_dir; let outcome = load_skills(&cfg); @@ -675,14 +683,14 @@ mod tests { assert_eq!(outcome.skills.len(), 0); } - #[test] - fn loads_skills_from_system_cache_when_present() { + #[tokio::test] + async fn loads_skills_from_system_cache_when_present() { let codex_home = tempfile::tempdir().expect("tempdir"); let work_dir = tempfile::tempdir().expect("tempdir"); write_system_skill(&codex_home, "system", "system-skill", "from system"); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = work_dir.path().to_path_buf(); let outcome = load_skills(&cfg); @@ -696,15 +704,15 @@ mod tests { assert_eq!(outcome.skills[0].scope, SkillScope::System); } - #[test] - fn deduplicates_by_name_preferring_user_over_system() { + #[tokio::test] + async fn deduplicates_by_name_preferring_user_over_system() { let codex_home = tempfile::tempdir().expect("tempdir"); let work_dir = tempfile::tempdir().expect("tempdir"); write_skill(&codex_home, "user", "dupe-skill", "from user"); write_system_skill(&codex_home, "system", "dupe-skill", "from system"); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = work_dir.path().to_path_buf(); let outcome = load_skills(&cfg); @@ -718,8 +726,8 @@ mod tests { assert_eq!(outcome.skills[0].scope, SkillScope::User); } - #[test] - fn deduplicates_by_name_preferring_repo_over_system() { + #[tokio::test] + async fn deduplicates_by_name_preferring_repo_over_system() { let codex_home = tempfile::tempdir().expect("tempdir"); let repo_dir = tempfile::tempdir().expect("tempdir"); @@ -741,7 +749,7 @@ mod tests { ); write_system_skill(&codex_home, "system", "dupe-skill", "from system"); - let mut cfg = make_config(&codex_home); + let mut cfg = make_config(&codex_home).await; cfg.cwd = repo_dir.path().to_path_buf(); let outcome = load_skills(&cfg); diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index bcc4ed9309..624094a5ad 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -358,9 +358,9 @@ mod tests { )); } - #[test] - fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_context() { - let (session, turn_context) = make_session_and_context(); + #[tokio::test] + async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_context() { + let (session, turn_context) = make_session_and_context().await; let command = "echo hello".to_string(); let workdir = Some("subdir".to_string()); diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 814001f41f..2cb30e5aa3 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -187,8 +187,8 @@ mod tests { use super::session::OutputBufferState; - fn test_session_and_turn() -> (Arc, Arc) { - let (session, mut turn) = make_session_and_context(); + async fn test_session_and_turn() -> (Arc, Arc) { + let (session, mut turn) = make_session_and_context().await; turn.approval_policy = AskForApproval::Never; turn.sandbox_policy = SandboxPolicy::DangerFullAccess; (Arc::new(session), Arc::new(turn)) @@ -266,7 +266,7 @@ mod tests { async fn unified_exec_persists_across_requests() -> anyhow::Result<()> { skip_if_sandbox!(Ok(())); - let (session, turn) = test_session_and_turn(); + let (session, turn) = test_session_and_turn().await; let open_shell = exec_command(&session, &turn, "bash -i", 2_500).await?; let process_id = open_shell @@ -302,7 +302,7 @@ mod tests { async fn multi_unified_exec_sessions() -> anyhow::Result<()> { skip_if_sandbox!(Ok(())); - let (session, turn) = test_session_and_turn(); + let (session, turn) = test_session_and_turn().await; let shell_a = exec_command(&session, &turn, "bash -i", 2_500).await?; let session_a = shell_a @@ -354,7 +354,7 @@ mod tests { async fn unified_exec_timeouts() -> anyhow::Result<()> { skip_if_sandbox!(Ok(())); - let (session, turn) = test_session_and_turn(); + let (session, turn) = test_session_and_turn().await; let open_shell = exec_command(&session, &turn, "bash -i", 2_500).await?; let process_id = open_shell @@ -398,7 +398,7 @@ mod tests { #[tokio::test] #[ignore] // Ignored while we have a better way to test this. async fn requests_with_large_timeout_are_capped() -> anyhow::Result<()> { - let (session, turn) = test_session_and_turn(); + let (session, turn) = test_session_and_turn().await; let result = exec_command(&session, &turn, "echo codex", 120_000).await?; @@ -411,7 +411,7 @@ mod tests { #[tokio::test] #[ignore] // Ignored while we have a better way to test this. async fn completed_commands_do_not_persist_sessions() -> anyhow::Result<()> { - let (session, turn) = test_session_and_turn(); + let (session, turn) = test_session_and_turn().await; let result = exec_command(&session, &turn, "echo codex", 2_500).await?; assert!( @@ -438,7 +438,7 @@ mod tests { async fn reusing_completed_session_returns_unknown_session() -> anyhow::Result<()> { skip_if_sandbox!(Ok(())); - let (session, turn) = test_session_and_turn(); + let (session, turn) = test_session_and_turn().await; let open_shell = exec_command(&session, &turn, "bash -i", 2_500).await?; let process_id = open_shell diff --git a/codex-rs/core/src/user_shell_command.rs b/codex-rs/core/src/user_shell_command.rs index 857e01c068..fb8efcc09c 100644 --- a/codex-rs/core/src/user_shell_command.rs +++ b/codex-rs/core/src/user_shell_command.rs @@ -80,8 +80,8 @@ mod tests { assert!(!is_user_shell_command_text("echo hi")); } - #[test] - fn formats_basic_record() { + #[tokio::test] + async fn formats_basic_record() { let exec_output = ExecToolCallOutput { exit_code: 0, stdout: StreamOutput::new("hi".to_string()), @@ -90,7 +90,7 @@ mod tests { duration: Duration::from_secs(1), timed_out: false, }; - let (_, turn_context) = make_session_and_context(); + let (_, turn_context) = make_session_and_context().await; let item = user_shell_command_record_item("echo hi", &exec_output, &turn_context); let ResponseItem::Message { content, .. } = item else { panic!("expected message"); @@ -104,8 +104,8 @@ mod tests { ); } - #[test] - fn uses_aggregated_output_over_streams() { + #[tokio::test] + async fn uses_aggregated_output_over_streams() { let exec_output = ExecToolCallOutput { exit_code: 42, stdout: StreamOutput::new("stdout-only".to_string()), @@ -114,7 +114,7 @@ mod tests { duration: Duration::from_millis(120), timed_out: false, }; - let (_, turn_context) = make_session_and_context(); + let (_, turn_context) = make_session_and_context().await; let record = format_user_shell_command_record("false", &exec_output, &turn_context); assert_eq!( record, diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index 3e53fa85cf..5867935470 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -65,7 +65,7 @@ async fn run_request(input: Vec) -> Value { Ok(dir) => dir, Err(e) => panic!("failed to create TempDir: {e}"), }; - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider_id = provider.name.clone(); config.model_provider = provider.clone(); config.show_raw_agent_reasoning = true; diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index 969fa47b86..f58b039220 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -64,7 +64,7 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { Ok(dir) => dir, Err(e) => panic!("failed to create TempDir: {e}"), }; - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider_id = provider.name.clone(); config.model_provider = provider.clone(); config.show_raw_agent_reasoning = true; diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 280b76dea1..a530a52034 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -4,8 +4,9 @@ use tempfile::TempDir; use codex_core::CodexConversation; use codex_core::config::Config; +use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; -use codex_core::config::ConfigToml; +use codex_core::config_loader::LoaderOverrides; use codex_utils_absolute_path::AbsolutePathBuf; use regex_lite::Regex; use std::path::PathBuf; @@ -75,13 +76,22 @@ pub fn test_tmp_path_buf() -> PathBuf { /// Returns a default `Config` whose on-disk state is confined to the provided /// temporary directory. Using a per-test directory keeps tests hermetic and /// avoids clobbering a developer’s real `~/.codex`. -pub fn load_default_config_for_test(codex_home: &TempDir) -> Config { - Config::load_from_base_config_with_overrides( - ConfigToml::default(), - default_test_overrides(), - codex_home.path().to_path_buf(), - ) - .expect("defaults for test should always succeed") +pub async fn load_default_config_for_test(codex_home: &TempDir) -> Config { + ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .harness_overrides(default_test_overrides()) + .loader_overrides(test_loader_overrides(codex_home)) + .build() + .await + .expect("defaults for test should always succeed") +} + +fn test_loader_overrides(codex_home: &TempDir) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.path().join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } } #[cfg(target_os = "linux")] diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 59379d7686..1e574cdef1 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -178,7 +178,7 @@ impl TestCodexBuilder { ..built_in_model_providers()["openai"].clone() }; let cwd = Arc::new(TempDir::new()?); - let mut config = load_default_config_for_test(home); + let mut config = load_default_config_for_test(home).await; config.cwd = cwd.path().to_path_buf(); config.model_provider = model_provider; for hook in self.pre_build_hooks.drain(..) { diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 382c8875ce..5c32685cc9 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -57,7 +57,7 @@ async fn responses_stream_includes_subagent_header_on_review() { }; let codex_home = TempDir::new().expect("failed to create TempDir"); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider_id = provider.name.clone(); config.model_provider = provider.clone(); let effort = config.model_reasoning_effort; @@ -151,7 +151,7 @@ async fn responses_stream_includes_subagent_header_on_other() { }; let codex_home = TempDir::new().expect("failed to create TempDir"); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider_id = provider.name.clone(); config.model_provider = provider.clone(); let effort = config.model_reasoning_effort; @@ -241,7 +241,7 @@ async fn responses_respects_model_family_overrides_from_config() { }; let codex_home = TempDir::new().expect("failed to create TempDir"); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model = Some("gpt-3.5-turbo".to_string()); config.model_provider_id = provider.name.clone(); config.model_provider = provider.clone(); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 35a67a6929..bda232433d 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -254,7 +254,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { ..built_in_model_providers()["openai"].clone() }; let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; // Also configure user instructions to ensure they are NOT delivered on resume. config.user_instructions = Some("be nice".to_string()); @@ -343,7 +343,7 @@ async fn includes_conversation_id_and_model_headers_in_request() { // Init session let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; let conversation_manager = ConversationManager::with_models_provider_and_home( @@ -403,7 +403,7 @@ async fn includes_base_instructions_override_in_request() { ..built_in_model_providers()["openai"].clone() }; let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.base_instructions = Some("test instructions".to_string()); config.model_provider = model_provider; @@ -467,7 +467,7 @@ async fn chatgpt_auth_sends_correct_request() { // Init session let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; let conversation_manager = ConversationManager::with_models_provider_and_home( create_dummy_codex_auth(), @@ -559,7 +559,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { Some("acc-123"), ); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; let auth_manager = @@ -602,7 +602,7 @@ async fn includes_user_instructions_message_in_request() { }; let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; config.user_instructions = Some("be nice".to_string()); @@ -671,7 +671,7 @@ async fn skills_append_to_instructions() { ) .expect("write skill"); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; config.cwd = codex_home.path().to_path_buf(); config.features.enable(Feature::Skills); @@ -1029,7 +1029,7 @@ async fn includes_developer_instructions_message_in_request() { }; let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; config.user_instructions = Some("be nice".to_string()); config.developer_instructions = Some("be useful".to_string()); @@ -1119,7 +1119,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { }; let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider_id = provider.name.clone(); config.model_provider = provider.clone(); let effort = config.model_reasoning_effort; @@ -1261,7 +1261,7 @@ async fn token_count_includes_rate_limits_snapshot() { provider.base_url = Some(format!("{}/v1", server.uri())); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = provider; let conversation_manager = ConversationManager::with_models_provider_and_home( @@ -1616,7 +1616,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() { // Init session let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = provider; let conversation_manager = ConversationManager::with_models_provider_and_home( @@ -1698,7 +1698,7 @@ async fn env_var_overrides_loaded_auth() { // Init session let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = provider; let conversation_manager = ConversationManager::with_models_provider_and_home( @@ -1780,7 +1780,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { // Init session with isolated codex home. let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; let conversation_manager = ConversationManager::with_models_provider_and_home( diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index dd8e4ca2c6..4f57330a28 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -137,7 +137,7 @@ async fn summarize_context_three_requests_and_instructions() { // Build config pointing to the mock server and spawn Codex. let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200_000); @@ -331,7 +331,7 @@ async fn manual_compact_uses_custom_prompt() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; config.compact_prompt = Some(custom_prompt.to_string()); @@ -411,7 +411,7 @@ async fn manual_compact_emits_api_and_local_token_usage_events() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); @@ -1062,7 +1062,7 @@ async fn auto_compact_runs_after_token_limit_hit() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200_000); @@ -1285,7 +1285,7 @@ async fn auto_compact_persists_rollout_entries() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200_000); @@ -1397,7 +1397,7 @@ async fn manual_compact_retries_after_context_window_error() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200_000); @@ -1530,7 +1530,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); let codex = ConversationManager::with_models_provider( @@ -1733,7 +1733,7 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_ let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200); @@ -1844,7 +1844,7 @@ async fn auto_compact_triggers_after_function_call_over_95_percent_usage() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_context_window = Some(context_window); diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 188e38da18..75468ae145 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -862,7 +862,7 @@ async fn start_test_conversation( ..built_in_model_providers()["openai"].clone() }; let home = TempDir::new().expect("create temp dir"); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; config.compact_prompt = Some(SUMMARIZATION_PROMPT.to_string()); if let Some(model) = model { diff --git a/codex-rs/core/tests/suite/fork_conversation.rs b/codex-rs/core/tests/suite/fork_conversation.rs index a82b476214..d302b4d77a 100644 --- a/codex-rs/core/tests/suite/fork_conversation.rs +++ b/codex-rs/core/tests/suite/fork_conversation.rs @@ -51,7 +51,7 @@ async fn fork_conversation_twice_drops_to_first_message() { }; let home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider.clone(); let config_for_fork = config.clone(); diff --git a/codex-rs/core/tests/suite/list_models.rs b/codex-rs/core/tests/suite/list_models.rs index 7db4cf76a5..35b847c2b2 100644 --- a/codex-rs/core/tests/suite/list_models.rs +++ b/codex-rs/core/tests/suite/list_models.rs @@ -12,7 +12,7 @@ use tempfile::tempdir; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn list_models_returns_api_key_models() -> Result<()> { let codex_home = tempdir()?; - let config = load_default_config_for_test(&codex_home); + let config = load_default_config_for_test(&codex_home).await; let manager = ConversationManager::with_models_provider( CodexAuth::from_api_key("sk-test"), built_in_model_providers()["openai"].clone(), @@ -28,7 +28,7 @@ async fn list_models_returns_api_key_models() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn list_models_returns_chatgpt_models() -> Result<()> { let codex_home = tempdir()?; - let config = load_default_config_for_test(&codex_home); + let config = load_default_config_for_test(&codex_home).await; let manager = ConversationManager::with_models_provider( CodexAuth::create_dummy_chatgpt_auth_for_testing(), built_in_model_providers()["openai"].clone(), diff --git a/codex-rs/core/tests/suite/model_overrides.rs b/codex-rs/core/tests/suite/model_overrides.rs index 53a45e6786..f7cdac67c1 100644 --- a/codex-rs/core/tests/suite/model_overrides.rs +++ b/codex-rs/core/tests/suite/model_overrides.rs @@ -19,7 +19,7 @@ async fn override_turn_context_does_not_persist_when_config_exists() { .await .expect("seed config.toml"); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model = Some("gpt-4o".to_string()); let conversation_manager = ConversationManager::with_models_provider( @@ -62,7 +62,7 @@ async fn override_turn_context_does_not_create_config_file() { "test setup should start without config" ); - let config = load_default_config_for_test(&codex_home); + let config = load_default_config_for_test(&codex_home).await; let conversation_manager = ConversationManager::with_models_provider( CodexAuth::from_api_key("Test API Key"), diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index f95eef7ad6..3c4d389ec0 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -316,7 +316,7 @@ async fn remote_models_preserve_builtin_presets() -> Result<()> { .await; let codex_home = TempDir::new()?; - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.features.enable(Feature::RemoteModels); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); @@ -374,7 +374,7 @@ async fn remote_models_hide_picker_only_models() -> Result<()> { .await; let codex_home = TempDir::new()?; - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.features.enable(Feature::RemoteModels); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); @@ -440,7 +440,7 @@ where let home = Arc::new(TempDir::new()?); let cwd = Arc::new(TempDir::new()?); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.cwd = cwd.path().to_path_buf(); config.features.enable(Feature::RemoteModels); diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index 4b6a133150..99fdafe08f 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -42,7 +42,7 @@ fn resume_history( async fn emits_warning_when_resumed_model_differs() { // Arrange a config with a current model and a prior rollout recorded under a different model. let home = TempDir::new().expect("tempdir"); - let mut config = load_default_config_for_test(&home); + let mut config = load_default_config_for_test(&home).await; config.model = Some("current-model".to_string()); // Ensure cwd is absolute (the helper sets it to the temp dir already). assert!(config.cwd.is_absolute()); diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index 4597c0f190..fba7af588c 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -453,7 +453,7 @@ async fn review_input_isolated_from_parent_history() { // Seed a parent session history via resume file with both user + assistant items. let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = ModelProviderInfo { base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() @@ -740,7 +740,7 @@ where base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let mut config = load_default_config_for_test(codex_home); + let mut config = load_default_config_for_test(codex_home).await; config.model_provider = model_provider; mutator(&mut config); let conversation_manager = ConversationManager::with_models_provider( @@ -769,7 +769,7 @@ where base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; - let mut config = load_default_config_for_test(codex_home); + let mut config = load_default_config_for_test(codex_home).await; config.model_provider = model_provider; mutator(&mut config); let conversation_manager = ConversationManager::with_models_provider( diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index 8472399ce4..270cb80487 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -39,7 +39,7 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() { // Load config and pin cwd to the temp dir so ls/cat operate there. let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); + let mut config = load_default_config_for_test(&codex_home).await; config.cwd = cwd.path().to_path_buf(); let conversation_manager = ConversationManager::with_models_provider( @@ -100,7 +100,7 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() { async fn user_shell_cmd_can_be_interrupted() { // Set up isolated config and conversation. let codex_home = TempDir::new().unwrap(); - let config = load_default_config_for_test(&codex_home); + let config = load_default_config_for_test(&codex_home).await; let conversation_manager = ConversationManager::with_models_provider( codex_core::CodexAuth::from_api_key("dummy"), config.model_provider.clone(), diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 55c12a3427..c3c043240e 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -8,10 +8,10 @@ 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::ConfigOverrides; -use codex_core::config::ConfigToml; +use codex_core::config::ConfigBuilder; use codex_core::config::Constrained; use codex_core::config::ConstraintError; +use codex_core::config_loader::LoaderOverrides; use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::AgentMessageDeltaEvent; use codex_core::protocol::AgentMessageEvent; @@ -63,6 +63,8 @@ use crossterm::event::KeyModifiers; use insta::assert_snapshot; use pretty_assertions::assert_eq; use std::collections::HashSet; +use std::future::Future; +use std::path::Path; use std::path::PathBuf; use tempfile::NamedTempFile; use tempfile::tempdir; @@ -76,15 +78,59 @@ fn set_windows_sandbox_enabled(enabled: bool) { fn test_config() -> Config { // Use base defaults to avoid depending on host state. - - Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - std::env::temp_dir(), + let codex_home = std::env::temp_dir(); + block_on( + ConfigBuilder::default() + .codex_home(codex_home.clone()) + .loader_overrides(test_loader_overrides(codex_home.as_path())) + .build(), ) .expect("config") } +fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } +} + +fn block_on(future: F) -> F::Output +where + F: Future + Send, + F::Output: Send, +{ + match tokio::runtime::Handle::try_current() { + Ok(handle) => { + if matches!( + handle.runtime_flavor(), + tokio::runtime::RuntimeFlavor::CurrentThread + ) { + std::thread::scope(|scope| { + scope + .spawn(|| { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime") + .block_on(future) + }) + .join() + .expect("join tokio thread") + }) + } else { + tokio::task::block_in_place(|| handle.block_on(future)) + } + } + Err(_) => tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime") + .block_on(future), + } +} + fn snapshot(percent: f64) -> RateLimitSnapshot { RateLimitSnapshot { primary: Some(RateLimitWindow { diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 2c0a37ecea..bedb94b881 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1580,10 +1580,10 @@ mod tests { use crate::exec_cell::ExecCall; use crate::exec_cell::ExecCell; use codex_core::config::Config; - use codex_core::config::ConfigOverrides; - use codex_core::config::ConfigToml; + use codex_core::config::ConfigBuilder; use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; + use codex_core::config_loader::LoaderOverrides; use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::McpAuthStatus; use codex_protocol::parse_command::ParsedCommand; @@ -1598,14 +1598,30 @@ mod tests { use mcp_types::TextContent; use mcp_types::Tool; use mcp_types::ToolInputSchema; + use std::path::Path; fn test_config() -> Config { - Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - std::env::temp_dir(), - ) - .expect("config") + let codex_home = std::env::temp_dir(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime"); + runtime + .block_on( + ConfigBuilder::default() + .codex_home(codex_home.clone()) + .loader_overrides(test_loader_overrides(codex_home.as_path())) + .build(), + ) + .expect("config") + } + + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } } fn render_lines(lines: &[Line<'static>]) -> Vec { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 005446c5f0..ce3a8e14a5 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -596,21 +596,34 @@ fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool #[cfg(test)] mod tests { use super::*; - use codex_core::config::ConfigOverrides; - use codex_core::config::ConfigToml; + use codex_core::config::ConfigBuilder; use codex_core::config::ProjectConfig; + use codex_core::config_loader::LoaderOverrides; use serial_test::serial; + use std::path::Path; use tempfile::TempDir; - #[test] + async fn build_config(temp_dir: &TempDir) -> std::io::Result { + ConfigBuilder::default() + .codex_home(temp_dir.path().to_path_buf()) + .loader_overrides(test_loader_overrides(temp_dir.path())) + .build() + .await + } + + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } + } + + #[tokio::test] #[serial] - fn windows_skips_trust_prompt_without_sandbox() -> std::io::Result<()> { + async fn windows_skips_trust_prompt_without_sandbox() -> std::io::Result<()> { let temp_dir = TempDir::new()?; - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - temp_dir.path().to_path_buf(), - )?; + let mut config = build_config(&temp_dir).await?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: None }; config.set_windows_sandbox_globally(false); @@ -629,15 +642,11 @@ mod tests { } Ok(()) } - #[test] + #[tokio::test] #[serial] - fn windows_shows_trust_prompt_with_sandbox() -> std::io::Result<()> { + async fn windows_shows_trust_prompt_with_sandbox() -> std::io::Result<()> { let temp_dir = TempDir::new()?; - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - temp_dir.path().to_path_buf(), - )?; + let mut config = build_config(&temp_dir).await?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: None }; config.set_windows_sandbox_globally(true); @@ -656,15 +665,11 @@ mod tests { } Ok(()) } - #[test] - fn untrusted_project_skips_trust_prompt() -> std::io::Result<()> { + #[tokio::test] + async fn untrusted_project_skips_trust_prompt() -> std::io::Result<()> { use codex_protocol::config_types::TrustLevel; let temp_dir = TempDir::new()?; - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - temp_dir.path().to_path_buf(), - )?; + let mut config = build_config(&temp_dir).await?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: Some(TrustLevel::Untrusted), diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 53c728526a..cd2dea695c 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -6,8 +6,8 @@ use chrono::TimeZone; use chrono::Utc; use codex_core::AuthManager; use codex_core::config::Config; -use codex_core::config::ConfigOverrides; -use codex_core::config::ConfigToml; +use codex_core::config::ConfigBuilder; +use codex_core::config_loader::LoaderOverrides; use codex_core::openai_models::model_family::ModelFamily; use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::CreditsSnapshot; @@ -19,16 +19,25 @@ use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ReasoningEffort; use insta::assert_snapshot; use ratatui::prelude::*; +use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; -fn test_config(temp_home: &TempDir) -> Config { - Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - temp_home.path().to_path_buf(), - ) - .expect("load config") +async fn test_config(temp_home: &TempDir) -> Config { + ConfigBuilder::default() + .codex_home(temp_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(temp_home.path())) + .build() + .await + .expect("load config") +} + +fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } } fn test_auth_manager(config: &Config) -> AuthManager { @@ -84,8 +93,8 @@ fn reset_at_from(captured_at: &chrono::DateTime, seconds: i64) -> .timestamp() } -#[test] -fn status_snapshot_includes_reasoning_details() { +#[tokio::test] +async fn status_snapshot_includes_reasoning_details() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model = Some("gpt-5.1-codex-max".to_string()); @@ -155,8 +164,8 @@ fn status_snapshot_includes_reasoning_details() { assert_snapshot!(sanitized); } -#[test] -fn status_snapshot_includes_monthly_limit() { +#[tokio::test] +async fn status_snapshot_includes_monthly_limit() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model = Some("gpt-5.1-codex-max".to_string()); @@ -212,8 +221,8 @@ fn status_snapshot_includes_monthly_limit() { assert_snapshot!(sanitized); } -#[test] -fn status_snapshot_shows_unlimited_credits() { +#[tokio::test] +async fn status_snapshot_shows_unlimited_credits() { let temp_home = TempDir::new().expect("temp home"); let config = test_config(&temp_home); let auth_manager = test_auth_manager(&config); @@ -256,8 +265,8 @@ fn status_snapshot_shows_unlimited_credits() { ); } -#[test] -fn status_snapshot_shows_positive_credits() { +#[tokio::test] +async fn status_snapshot_shows_positive_credits() { let temp_home = TempDir::new().expect("temp home"); let config = test_config(&temp_home); let auth_manager = test_auth_manager(&config); @@ -300,8 +309,8 @@ fn status_snapshot_shows_positive_credits() { ); } -#[test] -fn status_snapshot_hides_zero_credits() { +#[tokio::test] +async fn status_snapshot_hides_zero_credits() { let temp_home = TempDir::new().expect("temp home"); let config = test_config(&temp_home); let auth_manager = test_auth_manager(&config); @@ -342,8 +351,8 @@ fn status_snapshot_hides_zero_credits() { ); } -#[test] -fn status_snapshot_hides_when_has_no_credits_flag() { +#[tokio::test] +async fn status_snapshot_hides_when_has_no_credits_flag() { let temp_home = TempDir::new().expect("temp home"); let config = test_config(&temp_home); let auth_manager = test_auth_manager(&config); @@ -384,8 +393,8 @@ fn status_snapshot_hides_when_has_no_credits_flag() { ); } -#[test] -fn status_card_token_usage_excludes_cached_tokens() { +#[tokio::test] +async fn status_card_token_usage_excludes_cached_tokens() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model = Some("gpt-5.1-codex-max".to_string()); @@ -427,8 +436,8 @@ fn status_card_token_usage_excludes_cached_tokens() { ); } -#[test] -fn status_snapshot_truncates_in_narrow_terminal() { +#[tokio::test] +async fn status_snapshot_truncates_in_narrow_terminal() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model = Some("gpt-5.1-codex-max".to_string()); @@ -487,8 +496,8 @@ fn status_snapshot_truncates_in_narrow_terminal() { assert_snapshot!(sanitized); } -#[test] -fn status_snapshot_shows_missing_limits_message() { +#[tokio::test] +async fn status_snapshot_shows_missing_limits_message() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model = Some("gpt-5.1-codex-max".to_string()); @@ -532,8 +541,8 @@ fn status_snapshot_shows_missing_limits_message() { assert_snapshot!(sanitized); } -#[test] -fn status_snapshot_includes_credits_and_limits() { +#[tokio::test] +async fn status_snapshot_includes_credits_and_limits() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model = Some("gpt-5.1-codex".to_string()); @@ -596,8 +605,8 @@ fn status_snapshot_includes_credits_and_limits() { assert_snapshot!(sanitized); } -#[test] -fn status_snapshot_shows_empty_limits_message() { +#[tokio::test] +async fn status_snapshot_shows_empty_limits_message() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model = Some("gpt-5.1-codex-max".to_string()); @@ -648,8 +657,8 @@ fn status_snapshot_shows_empty_limits_message() { assert_snapshot!(sanitized); } -#[test] -fn status_snapshot_shows_stale_limits_message() { +#[tokio::test] +async fn status_snapshot_shows_stale_limits_message() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model = Some("gpt-5.1-codex-max".to_string()); @@ -709,8 +718,8 @@ fn status_snapshot_shows_stale_limits_message() { assert_snapshot!(sanitized); } -#[test] -fn status_snapshot_cached_limits_hide_credits_without_flag() { +#[tokio::test] +async fn status_snapshot_cached_limits_hide_credits_without_flag() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model = Some("gpt-5.1-codex".to_string()); @@ -774,8 +783,8 @@ fn status_snapshot_cached_limits_hide_credits_without_flag() { assert_snapshot!(sanitized); } -#[test] -fn status_context_window_uses_last_usage() { +#[tokio::test] +async fn status_context_window_uses_last_usage() { let temp_home = TempDir::new().expect("temp home"); let mut config = test_config(&temp_home); config.model_context_window = Some(272_000); diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index b90cc6e969..cffdf01605 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -8,9 +8,9 @@ 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::ConfigOverrides; -use codex_core::config::ConfigToml; +use codex_core::config::ConfigBuilder; use codex_core::config::Constrained; +use codex_core::config_loader::LoaderOverrides; use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::AgentMessageDeltaEvent; use codex_core::protocol::AgentMessageEvent; @@ -62,6 +62,8 @@ use crossterm::event::KeyModifiers; use insta::assert_snapshot; use pretty_assertions::assert_eq; use std::collections::HashSet; +use std::future::Future; +use std::path::Path; use std::path::PathBuf; use tempfile::NamedTempFile; use tempfile::tempdir; @@ -75,15 +77,59 @@ fn set_windows_sandbox_enabled(enabled: bool) { fn test_config() -> Config { // Use base defaults to avoid depending on host state. - - Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - std::env::temp_dir(), + let codex_home = std::env::temp_dir(); + block_on( + ConfigBuilder::default() + .codex_home(codex_home.clone()) + .loader_overrides(test_loader_overrides(codex_home.as_path())) + .build(), ) .expect("config") } +fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } +} + +fn block_on(future: F) -> F::Output +where + F: Future + Send, + F::Output: Send, +{ + match tokio::runtime::Handle::try_current() { + Ok(handle) => { + if matches!( + handle.runtime_flavor(), + tokio::runtime::RuntimeFlavor::CurrentThread + ) { + std::thread::scope(|scope| { + scope + .spawn(|| { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime") + .block_on(future) + }) + .join() + .expect("join tokio thread") + }) + } else { + tokio::task::block_in_place(|| handle.block_on(future)) + } + } + Err(_) => tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime") + .block_on(future), + } +} + fn snapshot(percent: f64) -> RateLimitSnapshot { RateLimitSnapshot { primary: Some(RateLimitWindow { diff --git a/codex-rs/tui2/src/history_cell.rs b/codex-rs/tui2/src/history_cell.rs index 5440040f6b..54ad936749 100644 --- a/codex-rs/tui2/src/history_cell.rs +++ b/codex-rs/tui2/src/history_cell.rs @@ -1514,10 +1514,10 @@ mod tests { use crate::exec_cell::ExecCall; use crate::exec_cell::ExecCell; use codex_core::config::Config; - use codex_core::config::ConfigOverrides; - use codex_core::config::ConfigToml; + use codex_core::config::ConfigBuilder; use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; + use codex_core::config_loader::LoaderOverrides; use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::McpAuthStatus; use codex_protocol::parse_command::ParsedCommand; @@ -1532,14 +1532,30 @@ mod tests { use mcp_types::TextContent; use mcp_types::Tool; use mcp_types::ToolInputSchema; + use std::path::Path; fn test_config() -> Config { - Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - std::env::temp_dir(), - ) - .expect("config") + let codex_home = std::env::temp_dir(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime"); + runtime + .block_on( + ConfigBuilder::default() + .codex_home(codex_home.clone()) + .loader_overrides(test_loader_overrides(codex_home.as_path())) + .build(), + ) + .expect("config") + } + + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } } fn render_lines(lines: &[Line<'static>]) -> Vec { diff --git a/codex-rs/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs index a9b34c495c..e582a6697e 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -622,21 +622,34 @@ fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool #[cfg(test)] mod tests { use super::*; - use codex_core::config::ConfigOverrides; - use codex_core::config::ConfigToml; + use codex_core::config::ConfigBuilder; use codex_core::config::ProjectConfig; + use codex_core::config_loader::LoaderOverrides; use serial_test::serial; + use std::path::Path; use tempfile::TempDir; - #[test] + async fn build_config(temp_dir: &TempDir) -> std::io::Result { + ConfigBuilder::default() + .codex_home(temp_dir.path().to_path_buf()) + .loader_overrides(test_loader_overrides(temp_dir.path())) + .build() + .await + } + + fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } + } + + #[tokio::test] #[serial] - fn windows_skips_trust_prompt_without_sandbox() -> std::io::Result<()> { + async fn windows_skips_trust_prompt_without_sandbox() -> std::io::Result<()> { let temp_dir = TempDir::new()?; - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - temp_dir.path().to_path_buf(), - )?; + let mut config = build_config(&temp_dir).await?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: None }; config.set_windows_sandbox_globally(false); @@ -655,15 +668,11 @@ mod tests { } Ok(()) } - #[test] + #[tokio::test] #[serial] - fn windows_shows_trust_prompt_with_sandbox() -> std::io::Result<()> { + async fn windows_shows_trust_prompt_with_sandbox() -> std::io::Result<()> { let temp_dir = TempDir::new()?; - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - temp_dir.path().to_path_buf(), - )?; + let mut config = build_config(&temp_dir).await?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: None }; config.set_windows_sandbox_globally(true); @@ -682,15 +691,11 @@ mod tests { } Ok(()) } - #[test] - fn untrusted_project_skips_trust_prompt() -> std::io::Result<()> { + #[tokio::test] + async fn untrusted_project_skips_trust_prompt() -> std::io::Result<()> { use codex_protocol::config_types::TrustLevel; let temp_dir = TempDir::new()?; - let mut config = Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - temp_dir.path().to_path_buf(), - )?; + let mut config = build_config(&temp_dir).await?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: Some(TrustLevel::Untrusted), diff --git a/codex-rs/tui2/src/status/tests.rs b/codex-rs/tui2/src/status/tests.rs index 53c728526a..4f9985bde0 100644 --- a/codex-rs/tui2/src/status/tests.rs +++ b/codex-rs/tui2/src/status/tests.rs @@ -6,8 +6,8 @@ use chrono::TimeZone; use chrono::Utc; use codex_core::AuthManager; use codex_core::config::Config; -use codex_core::config::ConfigOverrides; -use codex_core::config::ConfigToml; +use codex_core::config::ConfigBuilder; +use codex_core::config_loader::LoaderOverrides; use codex_core::openai_models::model_family::ModelFamily; use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::CreditsSnapshot; @@ -19,16 +19,31 @@ use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ReasoningEffort; use insta::assert_snapshot; use ratatui::prelude::*; +use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; fn test_config(temp_home: &TempDir) -> Config { - Config::load_from_base_config_with_overrides( - ConfigToml::default(), - ConfigOverrides::default(), - temp_home.path().to_path_buf(), - ) - .expect("load config") + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime"); + runtime + .block_on( + ConfigBuilder::default() + .codex_home(temp_home.path().to_path_buf()) + .loader_overrides(test_loader_overrides(temp_home.path())) + .build(), + ) + .expect("load config") +} + +fn test_loader_overrides(codex_home: &Path) -> LoaderOverrides { + LoaderOverrides { + managed_config_path: Some(codex_home.join("managed_config.toml")), + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + } } fn test_auth_manager(config: &Config) -> AuthManager {