diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 35f9068712..9818b2f61b 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -971,6 +971,7 @@ mod tests { match ConfigBuilder::default().build().await { Ok(config) => config, Err(_) => Config::load_default_with_cli_overrides(Vec::new()) + .await .expect("default config should load"), } } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 42cb8b2d1a..f805ec3ff6 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2442,6 +2442,7 @@ impl CodexMessageProcessor { )) { let trust_target = resolve_root_git_project_for_trust(config.cwd.as_path()) + .await .unwrap_or_else(|| config.cwd.to_path_buf()); let cli_overrides_with_trust; let cli_overrides_for_reload = if let Err(err) = diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index 84dfd282cd..4458bce89d 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -716,6 +716,7 @@ mod tests { match ConfigBuilder::default().build().await { Ok(config) => config, Err(_) => Config::load_default_with_cli_overrides(Vec::new()) + .await .expect("default config should load"), } } diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index ca48ad0f41..ba946c67b6 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -432,12 +432,14 @@ pub async fn run_main_with_transport( Err(err) => { let message = config_warning_from_error("Invalid configuration; using defaults.", &err); config_warnings.push(message); - Config::load_default_with_cli_overrides(cli_kv_overrides.clone()).map_err(|e| { - std::io::Error::new( - ErrorKind::InvalidData, - format!("error loading default config after config error: {e}"), - ) - })? + Config::load_default_with_cli_overrides(cli_kv_overrides.clone()) + .await + .map_err(|e| { + std::io::Error::new( + ErrorKind::InvalidData, + format!("error loading default config after config error: {e}"), + ) + })? } }; diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index b8d0db9a01..ab514252e4 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -717,6 +717,7 @@ model_reasoning_effort = "high" let config_toml = std::fs::read_to_string(codex_home.path().join("config.toml"))?; let trusted_root = resolve_root_git_project_for_trust(workspace.path()) + .await .unwrap_or_else(|| workspace.path().to_path_buf()); assert!(config_toml.contains(&persisted_trust_path(&trusted_root))); assert!(config_toml.contains("trust_level = \"trusted\"")); @@ -753,8 +754,9 @@ async fn thread_start_with_nested_git_cwd_trusts_repo_root() -> Result<()> { .await??; let config_toml = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - let trusted_root = - resolve_root_git_project_for_trust(&nested).expect("git root should resolve"); + let trusted_root = resolve_root_git_project_for_trust(&nested) + .await + .expect("git root should resolve"); assert!(config_toml.contains(&persisted_trust_path(&trusted_root))); assert!(!config_toml.contains(&persisted_trust_path(&nested))); diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 92e5304fe5..7de4e253e5 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -596,7 +596,7 @@ pub struct GhostSnapshotToml { impl ConfigToml { /// Derive the effective sandbox policy from the configuration. - pub fn derive_sandbox_policy( + pub async fn derive_sandbox_policy( &self, sandbox_mode_override: Option, profile_sandbox_mode: Option, @@ -610,11 +610,13 @@ impl ConfigToml { let resolved_sandbox_mode = sandbox_mode_override .or(profile_sandbox_mode) .or(self.sandbox_mode) - .or_else(|| { + .or(if sandbox_mode_was_explicit { + None + } else { // If no sandbox_mode is set but this directory has a trust decision, // default to workspace-write except on unsandboxed Windows where we // default to read-only. - self.get_active_project(resolved_cwd).and_then(|p| { + self.get_active_project(resolved_cwd).await.and_then(|p| { if p.is_trusted() || p.is_untrusted() { if cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled @@ -676,7 +678,8 @@ impl ConfigToml { /// Resolves the cwd to an existing project, or returns None if ConfigToml /// does not contain a project corresponding to cwd or a git repo for cwd - pub fn get_active_project(&self, resolved_cwd: &Path) -> Option { + pub async fn get_active_project(&self, resolved_cwd: &Path) -> Option { + let repo_root = resolve_root_git_project_for_trust(resolved_cwd).await; let projects = self.projects.clone().unwrap_or_default(); let resolved_cwd_key = project_trust_key(resolved_cwd); @@ -691,8 +694,8 @@ impl ConfigToml { // If cwd lives inside a git repo/worktree, check whether the root git project // (the primary repository working directory) is trusted. This lets // worktrees inherit trust from the main project. - if let Some(repo_root) = resolve_root_git_project_for_trust(resolved_cwd) { - let repo_root_key = project_trust_key(repo_root.as_path()); + if let Some(repo_root) = repo_root.as_deref() { + let repo_root_key = project_trust_key(repo_root); let repo_root_raw_key = repo_root.to_string_lossy().to_string(); if let Some(project_config_for_root) = projects .get(&repo_root_key) diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs index 83c2b1843b..61847f49ba 100644 --- a/codex-rs/core/src/agent/role.rs +++ b/codex-rs/core/src/agent/role.rs @@ -78,7 +78,8 @@ async fn apply_role_to_config_inner( role_layer_toml, preserve_current_profile, preserve_current_provider, - )?; + ) + .await?; Ok(()) } @@ -150,7 +151,7 @@ fn preservation_policy(config: &Config, role_layer_toml: &TomlValue) -> (bool, b mod reload { use super::*; - pub(super) fn build_next_config( + pub(super) async fn build_next_config( config: &Config, role_layer_toml: TomlValue, preserve_current_profile: bool, @@ -171,7 +172,8 @@ mod reload { reload_overrides(config, preserve_current_provider), config.codex_home.clone(), config_layer_stack, - )?; + ) + .await?; if preserve_current_profile { next_config.active_profile = config.active_profile.clone(); } diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 054e98be67..fcb1b58993 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -457,8 +457,8 @@ fn numbered_mcp_tools(count: usize) -> HashMap { .collect() } -fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig { - let config = test_config(); +async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests( "gpt-5-codex", &config.to_models_manager_config(), @@ -870,7 +870,7 @@ async fn get_base_instructions_no_user_content() { ]; let (session, _turn_context) = make_session_and_context().await; - let config = test_config(); + let config = test_config().await; for test_case in test_cases { let model_info = model_info_for_slug(test_case.slug, &config); @@ -1035,10 +1035,10 @@ fn collect_explicit_app_ids_from_skill_items_skips_plain_mentions_with_skill_con assert_eq!(connector_ids, HashSet::::new()); } -#[test] -fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() { - let config = test_config(); - let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true); +#[tokio::test] +async fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() { + let config = test_config().await; + let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await; let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1); let exposure = build_mcp_tool_exposure( @@ -1057,10 +1057,10 @@ fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() { assert!(exposure.deferred_tools.is_none()); } -#[test] -fn mcp_tool_exposure_searches_large_effective_tool_sets() { - let config = test_config(); - let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true); +#[tokio::test] +async fn mcp_tool_exposure_searches_large_effective_tool_sets() { + let config = test_config().await; + let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await; let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD); let exposure = build_mcp_tool_exposure( @@ -1083,10 +1083,10 @@ fn mcp_tool_exposure_searches_large_effective_tool_sets() { assert_eq!(deferred_tool_names, expected_tool_names); } -#[test] -fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overlap() { - let config = test_config(); - let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true); +#[tokio::test] +async fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overlap() { + let config = test_config().await; + let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await; let mut mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1); mcp_tools.extend([( "mcp__codex_apps__calendar_create_event".to_string(), diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index b2931e1055..650d792306 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -117,8 +117,8 @@ fn http_mcp(url: &str) -> McpServerConfig { } } -#[test] -fn load_config_normalizes_relative_cwd_override() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_normalizes_relative_cwd_override() -> std::io::Result<()> { let expected_cwd = AbsolutePathBuf::relative_to_current_dir("nested")?; let codex_home = tempdir()?; let config = Config::load_from_base_config_with_overrides( @@ -128,14 +128,15 @@ fn load_config_normalizes_relative_cwd_override() -> std::io::Result<()> { ..Default::default() }, codex_home.abs(), - )?; + ) + .await?; assert_eq!(config.cwd, expected_cwd); Ok(()) } -#[test] -fn load_config_records_global_agents_path() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_records_global_agents_path() -> std::io::Result<()> { let codex_home = tempdir()?; let global_agents_path = codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME); std::fs::write(&global_agents_path, "\n global instructions \n")?; @@ -144,7 +145,8 @@ fn load_config_records_global_agents_path() -> std::io::Result<()> { ConfigToml::default(), ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.user_instructions.as_deref(), @@ -157,8 +159,8 @@ fn load_config_records_global_agents_path() -> std::io::Result<()> { Ok(()) } -#[test] -fn load_config_records_preferred_global_agents_override_path() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_records_preferred_global_agents_override_path() -> std::io::Result<()> { let codex_home = tempdir()?; std::fs::write( codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME), @@ -171,7 +173,8 @@ fn load_config_records_preferred_global_agents_override_path() -> std::io::Resul ConfigToml::default(), ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.user_instructions.as_deref(), @@ -184,8 +187,8 @@ fn load_config_records_preferred_global_agents_override_path() -> std::io::Resul Ok(()) } -#[test] -fn test_toml_parsing() { +#[tokio::test] +async fn test_toml_parsing() { let history_with_persistence = r#" [history] persistence = "save-all" @@ -251,6 +254,7 @@ consolidation_model = "gpt-5" ConfigOverrides::default(), tempdir().expect("tempdir").abs(), ) + .await .expect("load config from memories settings"); assert_eq!( config.memories, @@ -376,13 +380,14 @@ fn config_toml_deserializes_model_availability_nux() { ); } -#[test] -fn runtime_config_defaults_model_availability_nux() { +#[tokio::test] +async fn runtime_config_defaults_model_availability_nux() { let cfg = Config::load_from_base_config_with_overrides( ConfigToml::default(), ConfigOverrides::default(), tempdir().expect("tempdir").abs(), ) + .await .expect("load config"); assert_eq!( @@ -462,8 +467,9 @@ allow_upstream_proxy = false ); } -#[test] -fn permissions_profiles_network_populates_runtime_network_proxy_spec() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_network_populates_runtime_network_proxy_spec() -> std::io::Result<()> +{ let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -497,7 +503,8 @@ fn permissions_profiles_network_populates_runtime_network_proxy_spec() -> std::i ..Default::default() }, codex_home.abs(), - )?; + ) + .await?; let network = config .permissions .network @@ -509,8 +516,9 @@ fn permissions_profiles_network_populates_runtime_network_proxy_spec() -> std::i Ok(()) } -#[test] -fn permissions_profiles_network_disabled_by_default_does_not_start_proxy() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_network_disabled_by_default_does_not_start_proxy() +-> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -547,14 +555,15 @@ fn permissions_profiles_network_disabled_by_default_does_not_start_proxy() -> st ..Default::default() }, codex_home.abs(), - )?; + ) + .await?; assert!(config.permissions.network.is_none()); Ok(()) } -#[test] -fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Result<()> { +#[tokio::test] +async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::create_dir_all(cwd.path().join("docs"))?; @@ -595,7 +604,8 @@ fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Re ..Default::default() }, codex_home.abs(), - )?; + ) + .await?; let memories_root = codex_home.path().join("memories").abs(); assert_eq!( @@ -647,8 +657,8 @@ fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Re Ok(()) } -#[test] -fn permissions_profiles_require_default_permissions() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_require_default_permissions() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -677,6 +687,7 @@ fn permissions_profiles_require_default_permissions() -> std::io::Result<()> { }, codex_home.abs(), ) + .await .expect_err("missing default_permissions should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); @@ -687,8 +698,8 @@ fn permissions_profiles_require_default_permissions() -> std::io::Result<()> { Ok(()) } -#[test] -fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -719,6 +730,7 @@ fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Resul }, codex_home.abs(), ) + .await .expect_err("writes outside the workspace root should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); @@ -730,8 +742,8 @@ fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Resul Ok(()) } -#[test] -fn permissions_profiles_reject_nested_entries_for_non_project_roots() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_reject_nested_entries_for_non_project_roots() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -764,6 +776,7 @@ fn permissions_profiles_reject_nested_entries_for_non_project_roots() -> std::io }, codex_home.abs(), ) + .await .expect_err("nested entries outside :project_roots should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); @@ -774,7 +787,9 @@ fn permissions_profiles_reject_nested_entries_for_non_project_roots() -> std::io Ok(()) } -fn load_workspace_permission_profile(profile: PermissionProfileToml) -> std::io::Result { +async fn load_workspace_permission_profile( + profile: PermissionProfileToml, +) -> std::io::Result { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -793,10 +808,11 @@ fn load_workspace_permission_profile(profile: PermissionProfileToml) -> std::io: }, codex_home.abs(), ) + .await } -#[test] -fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> { let config = load_workspace_permission_profile(PermissionProfileToml { filesystem: Some(FilesystemPermissionsToml { entries: BTreeMap::from([( @@ -805,7 +821,8 @@ fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> { )]), }), network: None, - })?; + }) + .await?; assert_eq!( config.permissions.file_system_sandbox_policy, @@ -839,8 +856,9 @@ fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> { Ok(()) } -#[test] -fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() +-> std::io::Result<()> { let config = load_workspace_permission_profile(PermissionProfileToml { filesystem: Some(FilesystemPermissionsToml { entries: BTreeMap::from([( @@ -852,7 +870,8 @@ fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() -> std )]), }), network: None, - })?; + }) + .await?; assert_eq!( config.permissions.file_system_sandbox_policy, @@ -873,12 +892,13 @@ fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() -> std Ok(()) } -#[test] -fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io::Result<()> { let config = load_workspace_permission_profile(PermissionProfileToml { filesystem: None, network: None, - })?; + }) + .await?; assert_eq!( config.permissions.file_system_sandbox_policy, @@ -904,14 +924,15 @@ fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io::Resu Ok(()) } -#[test] -fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::Result<()> { let config = load_workspace_permission_profile(PermissionProfileToml { filesystem: Some(FilesystemPermissionsToml { entries: BTreeMap::new(), }), network: None, - })?; + }) + .await?; assert_eq!( config.permissions.file_system_sandbox_policy, @@ -927,8 +948,8 @@ fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::Result Ok(()) } -#[test] -fn permissions_profiles_reject_project_root_parent_traversal() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_reject_project_root_parent_traversal() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -961,6 +982,7 @@ fn permissions_profiles_reject_project_root_parent_traversal() -> std::io::Resul }, codex_home.abs(), ) + .await .expect_err("parent traversal should be rejected for project root subpaths"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); @@ -971,8 +993,8 @@ fn permissions_profiles_reject_project_root_parent_traversal() -> std::io::Resul Ok(()) } -#[test] -fn permissions_profiles_allow_network_enablement() -> std::io::Result<()> { +#[tokio::test] +async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -1004,7 +1026,8 @@ fn permissions_profiles_allow_network_enablement() -> std::io::Result<()> { ..Default::default() }, codex_home.abs(), - )?; + ) + .await?; assert!( config.permissions.network_sandbox_policy.is_enabled(), @@ -1067,8 +1090,8 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() { ); } -#[test] -fn test_sandbox_config_parsing() { +#[tokio::test] +async fn test_sandbox_config_parsing() { let sandbox_full_access = r#" sandbox_mode = "danger-full-access" @@ -1078,13 +1101,15 @@ network_access = false # This should be ignored. let sandbox_full_access_cfg = toml::from_str::(sandbox_full_access) .expect("TOML deserialization should succeed"); let sandbox_mode_override = None; - let resolution = sandbox_full_access_cfg.derive_sandbox_policy( - sandbox_mode_override, - /*profile_sandbox_mode*/ None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - /*sandbox_policy_constraint*/ None, - ); + let resolution = sandbox_full_access_cfg + .derive_sandbox_policy( + sandbox_mode_override, + /*profile_sandbox_mode*/ None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + /*sandbox_policy_constraint*/ None, + ) + .await; assert_eq!(resolution, SandboxPolicy::DangerFullAccess); let sandbox_read_only = r#" @@ -1097,13 +1122,15 @@ network_access = true # This should be ignored. let sandbox_read_only_cfg = toml::from_str::(sandbox_read_only) .expect("TOML deserialization should succeed"); let sandbox_mode_override = None; - let resolution = sandbox_read_only_cfg.derive_sandbox_policy( - sandbox_mode_override, - /*profile_sandbox_mode*/ None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - /*sandbox_policy_constraint*/ None, - ); + let resolution = sandbox_read_only_cfg + .derive_sandbox_policy( + sandbox_mode_override, + /*profile_sandbox_mode*/ None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + /*sandbox_policy_constraint*/ None, + ) + .await; assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); let writable_root = test_absolute_path("/my/workspace"); @@ -1124,13 +1151,15 @@ exclude_slash_tmp = true let sandbox_workspace_write_cfg = toml::from_str::(&sandbox_workspace_write) .expect("TOML deserialization should succeed"); let sandbox_mode_override = None; - let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy( - sandbox_mode_override, - /*profile_sandbox_mode*/ None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - /*sandbox_policy_constraint*/ None, - ); + let resolution = sandbox_workspace_write_cfg + .derive_sandbox_policy( + sandbox_mode_override, + /*profile_sandbox_mode*/ None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + /*sandbox_policy_constraint*/ None, + ) + .await; if cfg!(target_os = "windows") { assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); } else { @@ -1166,13 +1195,15 @@ trust_level = "trusted" let sandbox_workspace_write_cfg = toml::from_str::(&sandbox_workspace_write) .expect("TOML deserialization should succeed"); let sandbox_mode_override = None; - let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy( - sandbox_mode_override, - /*profile_sandbox_mode*/ None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - /*sandbox_policy_constraint*/ None, - ); + let resolution = sandbox_workspace_write_cfg + .derive_sandbox_policy( + sandbox_mode_override, + /*profile_sandbox_mode*/ None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + /*sandbox_policy_constraint*/ None, + ) + .await; if cfg!(target_os = "windows") { assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); } else { @@ -1189,8 +1220,8 @@ trust_level = "trusted" } } -#[test] -fn legacy_sandbox_mode_config_builds_split_policies_without_drift() -> std::io::Result<()> { +#[tokio::test] +async fn legacy_sandbox_mode_config_builds_split_policies_without_drift() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; let extra_root = test_absolute_path("/tmp/legacy-extra-root"); @@ -1232,7 +1263,8 @@ exclude_slash_tmp = true ..Default::default() }, codex_home.abs(), - )?; + ) + .await?; let sandbox_policy = config.permissions.sandbox_policy.get(); assert_eq!( @@ -1393,8 +1425,8 @@ fn filter_mcp_servers_by_allowlist_blocks_all_when_empty() { ); } -#[test] -fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> { +#[tokio::test] +async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> { let temp_dir = TempDir::new()?; let frontend = temp_dir.path().join("frontend"); let backend = temp_dir.path().join("backend"); @@ -1412,7 +1444,8 @@ fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> { ConfigToml::default(), overrides, temp_dir.path().abs(), - )?; + ) + .await?; let expected_backend = backend.abs(); if cfg!(target_os = "windows") { @@ -1440,8 +1473,8 @@ fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> { Ok(()) } -#[test] -fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Result<()> { +#[tokio::test] +async fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Result<()> { let codex_home = TempDir::new()?; let config = Config::load_from_base_config_with_overrides( ConfigToml::default(), @@ -1450,15 +1483,16 @@ fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Result<( ..Default::default() }, codex_home.abs(), - )?; + ) + .await?; assert_eq!(config.sqlite_home, codex_home.path().to_path_buf()); Ok(()) } -#[test] -fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> { +#[tokio::test] +async fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> { let codex_home = TempDir::new()?; let memories_root = codex_home.path().join("memories"); let config = Config::load_from_base_config_with_overrides( @@ -1474,7 +1508,8 @@ fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> { ..Default::default() }, codex_home.abs(), - )?; + ) + .await?; if cfg!(target_os = "windows") { match config.permissions.sandbox_policy.get() { @@ -1507,8 +1542,8 @@ fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> { Ok(()) } -#[test] -fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> { +#[tokio::test] +async fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml::default(); @@ -1516,7 +1551,8 @@ fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.cli_auth_credentials_store_mode, @@ -1526,8 +1562,8 @@ fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> { Ok(()) } -#[test] -fn config_resolves_explicit_keyring_auth_store_mode() -> std::io::Result<()> { +#[tokio::test] +async fn config_resolves_explicit_keyring_auth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { cli_auth_credentials_store: Some(AuthCredentialsStoreMode::Keyring), @@ -1538,7 +1574,8 @@ fn config_resolves_explicit_keyring_auth_store_mode() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.cli_auth_credentials_store_mode, @@ -1551,8 +1588,8 @@ fn config_resolves_explicit_keyring_auth_store_mode() -> std::io::Result<()> { Ok(()) } -#[test] -fn config_resolves_default_oauth_store_mode() -> std::io::Result<()> { +#[tokio::test] +async fn config_resolves_default_oauth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml::default(); @@ -1560,7 +1597,8 @@ fn config_resolves_default_oauth_store_mode() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.mcp_oauth_credentials_store_mode, @@ -1624,8 +1662,8 @@ fn local_dev_builds_force_file_mcp_oauth_store_modes() { ); } -#[test] -fn feedback_enabled_defaults_to_true() -> std::io::Result<()> { +#[tokio::test] +async fn feedback_enabled_defaults_to_true() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { feedback: Some(FeedbackConfigToml::default()), @@ -1636,7 +1674,8 @@ fn feedback_enabled_defaults_to_true() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!(config.feedback_enabled, true); @@ -1776,8 +1815,8 @@ profile = "project" Ok(()) } -#[test] -fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> { +#[tokio::test] +async fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> { let codex_home = TempDir::new()?; let mut profiles = HashMap::new(); profiles.insert( @@ -1798,7 +1837,8 @@ fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert!(matches!( config.permissions.sandbox_policy.get(), @@ -1808,8 +1848,8 @@ fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> { Ok(()) } -#[test] -fn cli_override_takes_precedence_over_profile_sandbox_mode() -> std::io::Result<()> { +#[tokio::test] +async fn cli_override_takes_precedence_over_profile_sandbox_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; let mut profiles = HashMap::new(); profiles.insert( @@ -1830,7 +1870,8 @@ fn cli_override_takes_precedence_over_profile_sandbox_mode() -> std::io::Result< ..Default::default() }; - let config = Config::load_from_base_config_with_overrides(cfg, overrides, codex_home.abs())?; + let config = + Config::load_from_base_config_with_overrides(cfg, overrides, codex_home.abs()).await?; if cfg!(target_os = "windows") { assert!(matches!( @@ -1847,8 +1888,8 @@ fn cli_override_takes_precedence_over_profile_sandbox_mode() -> std::io::Result< Ok(()) } -#[test] -fn feature_table_overrides_legacy_flags() -> std::io::Result<()> { +#[tokio::test] +async fn feature_table_overrides_legacy_flags() -> std::io::Result<()> { let codex_home = TempDir::new()?; let mut entries = BTreeMap::new(); entries.insert("apply_patch_freeform".to_string(), false); @@ -1861,7 +1902,8 @@ fn feature_table_overrides_legacy_flags() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert!(!config.features.enabled(Feature::ApplyPatchFreeform)); assert!(!config.include_apply_patch_tool); @@ -1869,8 +1911,8 @@ fn feature_table_overrides_legacy_flags() -> std::io::Result<()> { Ok(()) } -#[test] -fn legacy_toggles_map_to_features() -> std::io::Result<()> { +#[tokio::test] +async fn legacy_toggles_map_to_features() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { experimental_use_unified_exec_tool: Some(true), @@ -1882,7 +1924,8 @@ fn legacy_toggles_map_to_features() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert!(config.features.enabled(Feature::ApplyPatchFreeform)); assert!(config.features.enabled(Feature::UnifiedExec)); @@ -1894,8 +1937,8 @@ fn legacy_toggles_map_to_features() -> std::io::Result<()> { Ok(()) } -#[test] -fn responses_websocket_features_do_not_change_wire_api() -> std::io::Result<()> { +#[tokio::test] +async fn responses_websocket_features_do_not_change_wire_api() -> std::io::Result<()> { for feature_key in ["responses_websockets", "responses_websockets_v2"] { let codex_home = TempDir::new()?; let mut entries = BTreeMap::new(); @@ -1909,7 +1952,8 @@ fn responses_websocket_features_do_not_change_wire_api() -> std::io::Result<()> cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!(config.model_provider.wire_api, WireApi::Responses); } @@ -1917,8 +1961,8 @@ fn responses_websocket_features_do_not_change_wire_api() -> std::io::Result<()> Ok(()) } -#[test] -fn config_honors_explicit_file_oauth_store_mode() -> std::io::Result<()> { +#[tokio::test] +async fn config_honors_explicit_file_oauth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { mcp_oauth_credentials_store: Some(OAuthCredentialsStoreMode::File), @@ -1929,7 +1973,8 @@ fn config_honors_explicit_file_oauth_store_mode() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.mcp_oauth_credentials_store_mode, @@ -1974,7 +2019,8 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( final_config.mcp_oauth_credentials_store_mode, resolve_mcp_oauth_credentials_store_mode( @@ -2199,7 +2245,8 @@ async fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<( ConfigToml::default(), ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); let mcp_config = config.to_mcp_config(&plugins_manager).await; @@ -3248,8 +3295,8 @@ impl PrecedenceTestFixture { } } -#[test] -fn cli_override_sets_compact_prompt() -> std::io::Result<()> { +#[tokio::test] +async fn cli_override_sets_compact_prompt() -> std::io::Result<()> { let codex_home = TempDir::new()?; let overrides = ConfigOverrides { compact_prompt: Some("Use the compact override".to_string()), @@ -3260,7 +3307,8 @@ fn cli_override_sets_compact_prompt() -> std::io::Result<()> { ConfigToml::default(), overrides, codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.compact_prompt.as_deref(), @@ -3270,8 +3318,8 @@ fn cli_override_sets_compact_prompt() -> std::io::Result<()> { Ok(()) } -#[test] -fn loads_compact_prompt_from_file() -> std::io::Result<()> { +#[tokio::test] +async fn loads_compact_prompt_from_file() -> std::io::Result<()> { let codex_home = TempDir::new()?; let workspace = codex_home.path().join("workspace"); std::fs::create_dir_all(&workspace)?; @@ -3289,7 +3337,8 @@ fn loads_compact_prompt_from_file() -> std::io::Result<()> { ..Default::default() }; - let config = Config::load_from_base_config_with_overrides(cfg, overrides, codex_home.abs())?; + let config = + Config::load_from_base_config_with_overrides(cfg, overrides, codex_home.abs()).await?; assert_eq!( config.compact_prompt.as_deref(), @@ -3299,8 +3348,8 @@ fn loads_compact_prompt_from_file() -> std::io::Result<()> { Ok(()) } -#[test] -fn load_config_uses_requirements_guardian_policy_config() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_uses_requirements_guardian_policy_config() -> std::io::Result<()> { let codex_home = TempDir::new()?; let config_layer_stack = ConfigLayerStack::new( Vec::new(), @@ -3322,7 +3371,8 @@ fn load_config_uses_requirements_guardian_policy_config() -> std::io::Result<()> }, codex_home.abs(), config_layer_stack, - )?; + ) + .await?; assert_eq!( config.guardian_policy_config.as_deref(), @@ -3332,8 +3382,8 @@ fn load_config_uses_requirements_guardian_policy_config() -> std::io::Result<()> Ok(()) } -#[test] -fn load_config_ignores_empty_requirements_guardian_policy_config() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_ignores_empty_requirements_guardian_policy_config() -> std::io::Result<()> { let codex_home = TempDir::new()?; let config_layer_stack = ConfigLayerStack::new( Vec::new(), @@ -3353,15 +3403,16 @@ fn load_config_ignores_empty_requirements_guardian_policy_config() -> std::io::R }, codex_home.abs(), config_layer_stack, - )?; + ) + .await?; assert_eq!(config.guardian_policy_config, None); Ok(()) } -#[test] -fn load_config_rejects_missing_agent_role_config_file() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_rejects_missing_agent_role_config_file() -> std::io::Result<()> { let codex_home = TempDir::new()?; let missing_path = codex_home.path().join("agents").join("researcher.toml"); let cfg = ConfigToml { @@ -3385,7 +3436,8 @@ fn load_config_rejects_missing_agent_role_config_file() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - ); + ) + .await; let err = result.expect_err("missing role config file should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); let message = err.to_string(); @@ -4226,8 +4278,8 @@ model = "gpt-5-mini" Ok(()) } -#[test] -fn load_config_normalizes_agent_role_nickname_candidates() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_normalizes_agent_role_nickname_candidates() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { agents: Some(AgentsToml { @@ -4253,7 +4305,8 @@ fn load_config_normalizes_agent_role_nickname_candidates() -> std::io::Result<() cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config @@ -4267,8 +4320,8 @@ fn load_config_normalizes_agent_role_nickname_candidates() -> std::io::Result<() Ok(()) } -#[test] -fn load_config_rejects_empty_agent_role_nickname_candidates() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_rejects_empty_agent_role_nickname_candidates() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { agents: Some(AgentsToml { @@ -4291,7 +4344,8 @@ fn load_config_rejects_empty_agent_role_nickname_candidates() -> std::io::Result cfg, ConfigOverrides::default(), codex_home.abs(), - ); + ) + .await; let err = result.expect_err("empty nickname candidates should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); assert!( @@ -4302,8 +4356,8 @@ fn load_config_rejects_empty_agent_role_nickname_candidates() -> std::io::Result Ok(()) } -#[test] -fn load_config_rejects_duplicate_agent_role_nickname_candidates() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_rejects_duplicate_agent_role_nickname_candidates() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { agents: Some(AgentsToml { @@ -4326,7 +4380,8 @@ fn load_config_rejects_duplicate_agent_role_nickname_candidates() -> std::io::Re cfg, ConfigOverrides::default(), codex_home.abs(), - ); + ) + .await; let err = result.expect_err("duplicate nickname candidates should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); assert!( @@ -4337,8 +4392,8 @@ fn load_config_rejects_duplicate_agent_role_nickname_candidates() -> std::io::Re Ok(()) } -#[test] -fn load_config_rejects_unsafe_agent_role_nickname_candidates() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_rejects_unsafe_agent_role_nickname_candidates() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { agents: Some(AgentsToml { @@ -4361,7 +4416,8 @@ fn load_config_rejects_unsafe_agent_role_nickname_candidates() -> std::io::Resul cfg, ConfigOverrides::default(), codex_home.abs(), - ); + ) + .await; let err = result.expect_err("unsafe nickname candidates should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); assert!(err.to_string().contains( @@ -4371,8 +4427,8 @@ fn load_config_rejects_unsafe_agent_role_nickname_candidates() -> std::io::Resul Ok(()) } -#[test] -fn model_catalog_json_loads_from_path() -> std::io::Result<()> { +#[tokio::test] +async fn model_catalog_json_loads_from_path() -> std::io::Result<()> { let codex_home = TempDir::new()?; let catalog_path = codex_home.path().join("catalog.json"); let mut catalog = bundled_models_response() @@ -4392,14 +4448,15 @@ fn model_catalog_json_loads_from_path() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!(config.model_catalog, Some(catalog)); Ok(()) } -#[test] -fn model_catalog_json_rejects_empty_catalog() -> std::io::Result<()> { +#[tokio::test] +async fn model_catalog_json_rejects_empty_catalog() -> std::io::Result<()> { let codex_home = TempDir::new()?; let catalog_path = codex_home.path().join("catalog.json"); std::fs::write(&catalog_path, r#"{"models":[]}"#)?; @@ -4414,6 +4471,7 @@ fn model_catalog_json_rejects_empty_catalog() -> std::io::Result<()> { ConfigOverrides::default(), codex_home.abs(), ) + .await .expect_err("empty custom catalog should fail config load"); assert_eq!(err.kind(), ErrorKind::InvalidData); @@ -4538,8 +4596,8 @@ model_verbosity = "high" /// /// Note that profiles are the recommended way to specify a group of /// configuration options together. -#[test] -fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { +#[tokio::test] +async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { let fixture = create_test_fixture()?; let o3_profile_overrides = ConfigOverrides { @@ -4551,7 +4609,8 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { fixture.cfg.clone(), o3_profile_overrides, fixture.codex_home(), - )?; + ) + .await?; assert_eq!( Config { model: Some("o3".to_string()), @@ -4671,8 +4730,8 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { Ok(()) } -#[test] -fn metrics_exporter_defaults_to_statsig_when_missing() -> std::io::Result<()> { +#[tokio::test] +async fn metrics_exporter_defaults_to_statsig_when_missing() -> std::io::Result<()> { let fixture = create_test_fixture()?; let config = Config::load_from_base_config_with_overrides( @@ -4682,14 +4741,15 @@ fn metrics_exporter_defaults_to_statsig_when_missing() -> std::io::Result<()> { ..Default::default() }, fixture.codex_home(), - )?; + ) + .await?; assert_eq!(config.otel.metrics_exporter, OtelExporterKind::Statsig); Ok(()) } -#[test] -fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { +#[tokio::test] +async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { let fixture = create_test_fixture()?; let gpt3_profile_overrides = ConfigOverrides { @@ -4701,7 +4761,8 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { fixture.cfg.clone(), gpt3_profile_overrides, fixture.codex_home(), - )?; + ) + .await?; let expected_gpt3_profile_config = Config { model: Some("gpt-3.5-turbo".to_string()), review_model: None, @@ -4829,14 +4890,15 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { fixture.cfg.clone(), default_profile_overrides, fixture.codex_home(), - )?; + ) + .await?; assert_eq!(expected_gpt3_profile_config, default_profile_config); Ok(()) } -#[test] -fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { +#[tokio::test] +async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { let fixture = create_test_fixture()?; let zdr_profile_overrides = ConfigOverrides { @@ -4848,7 +4910,8 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { fixture.cfg.clone(), zdr_profile_overrides, fixture.codex_home(), - )?; + ) + .await?; let expected_zdr_profile_config = Config { model: Some("o3".to_string()), review_model: None, @@ -4968,8 +5031,8 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { Ok(()) } -#[test] -fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { +#[tokio::test] +async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { let fixture = create_test_fixture()?; let gpt5_profile_overrides = ConfigOverrides { @@ -4981,7 +5044,8 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { fixture.cfg.clone(), gpt5_profile_overrides, fixture.codex_home(), - )?; + ) + .await?; let expected_gpt5_profile_config = Config { model: Some("gpt-5.1".to_string()), review_model: None, @@ -5101,8 +5165,9 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { Ok(()) } -#[test] -fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> anyhow::Result<()> { +#[tokio::test] +async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> anyhow::Result<()> +{ let fixture = create_test_fixture()?; let requirements_toml = crate::config_loader::ConfigRequirementsToml { @@ -5154,7 +5219,8 @@ fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> any }, fixture.codex_home(), config_layer_stack, - )?; + ) + .await?; assert!( !config @@ -5317,9 +5383,9 @@ fn test_set_default_oss_provider_rejects_legacy_ollama_chat_provider() -> std::i Ok(()) } -#[test] -fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error() -> std::io::Result<()> -{ +#[tokio::test] +async fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error() +-> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { model_provider: Some(LEGACY_OLLAMA_CHAT_PROVIDER_ID.to_string()), @@ -5330,7 +5396,8 @@ fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error() -> cfg, ConfigOverrides::default(), codex_home.abs(), - ); + ) + .await; assert!(result.is_err()); let error = result.unwrap_err(); assert_eq!(error.kind(), std::io::ErrorKind::NotFound); @@ -5343,8 +5410,8 @@ fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error() -> Ok(()) } -#[test] -fn test_untrusted_project_gets_workspace_write_sandbox() -> anyhow::Result<()> { +#[tokio::test] +async fn test_untrusted_project_gets_workspace_write_sandbox() -> anyhow::Result<()> { let config_with_untrusted = r#" [projects."/tmp/test"] trust_level = "untrusted" @@ -5353,13 +5420,15 @@ trust_level = "untrusted" let cfg = toml::from_str::(config_with_untrusted) .expect("TOML deserialization should succeed"); - let resolution = cfg.derive_sandbox_policy( - /*sandbox_mode_override*/ None, - /*profile_sandbox_mode*/ None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - /*sandbox_policy_constraint*/ None, - ); + let resolution = cfg + .derive_sandbox_policy( + /*sandbox_mode_override*/ None, + /*profile_sandbox_mode*/ None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + /*sandbox_policy_constraint*/ None, + ) + .await; // Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade) if cfg!(target_os = "windows") { @@ -5377,9 +5446,9 @@ trust_level = "untrusted" Ok(()) } -#[test] -fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults() -> anyhow::Result<()> -{ +#[tokio::test] +async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults() +-> anyhow::Result<()> { let project_dir = TempDir::new()?; let project_path = project_dir.path().to_path_buf(); let project_key = project_path.to_string_lossy().to_string(); @@ -5405,21 +5474,23 @@ fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults() } })?; - let resolution = cfg.derive_sandbox_policy( - /*sandbox_mode_override*/ None, - /*profile_sandbox_mode*/ None, - WindowsSandboxLevel::Disabled, - &project_path, - Some(&constrained), - ); + let resolution = cfg + .derive_sandbox_policy( + /*sandbox_mode_override*/ None, + /*profile_sandbox_mode*/ None, + WindowsSandboxLevel::Disabled, + &project_path, + Some(&constrained), + ) + .await; assert_eq!(resolution, SandboxPolicy::DangerFullAccess); Ok(()) } -#[test] -fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallback() -> anyhow::Result<()> -{ +#[tokio::test] +async fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallback() +-> anyhow::Result<()> { let project_dir = TempDir::new()?; let project_path = project_dir.path().to_path_buf(); let project_key = project_path.to_string_lossy().to_string(); @@ -5445,13 +5516,15 @@ fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallback() } })?; - let resolution = cfg.derive_sandbox_policy( - /*sandbox_mode_override*/ None, - /*profile_sandbox_mode*/ None, - WindowsSandboxLevel::Disabled, - &project_path, - Some(&constrained), - ); + let resolution = cfg + .derive_sandbox_policy( + /*sandbox_mode_override*/ None, + /*profile_sandbox_mode*/ None, + WindowsSandboxLevel::Disabled, + &project_path, + Some(&constrained), + ) + .await; if cfg!(target_os = "windows") { assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); @@ -5579,8 +5652,8 @@ fn config_toml_deserializes_mcp_oauth_callback_url() { ); } -#[test] -fn config_loads_mcp_oauth_callback_port_from_toml() -> std::io::Result<()> { +#[tokio::test] +async fn config_loads_mcp_oauth_callback_port_from_toml() -> std::io::Result<()> { let codex_home = TempDir::new()?; let toml = r#" model = "gpt-5.1" @@ -5593,14 +5666,15 @@ mcp_oauth_callback_port = 5678 cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!(config.mcp_oauth_callback_port, Some(5678)); Ok(()) } -#[test] -fn config_loads_allow_login_shell_from_toml() -> std::io::Result<()> { +#[tokio::test] +async fn config_loads_allow_login_shell_from_toml() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg: ConfigToml = toml::from_str( r#" @@ -5614,14 +5688,15 @@ allow_login_shell = false cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert!(!config.permissions.allow_login_shell); Ok(()) } -#[test] -fn config_loads_mcp_oauth_callback_url_from_toml() -> std::io::Result<()> { +#[tokio::test] +async fn config_loads_mcp_oauth_callback_url_from_toml() -> std::io::Result<()> { let codex_home = TempDir::new()?; let toml = r#" model = "gpt-5.1" @@ -5634,7 +5709,8 @@ mcp_oauth_callback_url = "https://example.com/callback" cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.mcp_oauth_callback_url.as_deref(), @@ -5643,8 +5719,8 @@ mcp_oauth_callback_url = "https://example.com/callback" Ok(()) } -#[test] -fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow::Result<()> { +#[tokio::test] +async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow::Result<()> { let codex_home = TempDir::new()?; let test_project_dir = TempDir::new()?; let test_path = test_project_dir.path(); @@ -5664,7 +5740,8 @@ fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow::Resul ..Default::default() }, codex_home.abs(), - )?; + ) + .await?; // Verify that untrusted projects get UnlessTrusted approval policy assert_eq!( @@ -6359,8 +6436,8 @@ async fn feature_requirements_reject_collab_legacy_alias() { ); } -#[test] -fn tool_suggest_discoverables_load_from_config_toml() -> std::io::Result<()> { +#[tokio::test] +async fn tool_suggest_discoverables_load_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" [tool_suggest] @@ -6398,7 +6475,8 @@ discoverables = [ cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.tool_suggest, @@ -6418,8 +6496,8 @@ discoverables = [ Ok(()) } -#[test] -fn experimental_realtime_start_instructions_load_from_config_toml() -> std::io::Result<()> { +#[tokio::test] +async fn experimental_realtime_start_instructions_load_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" experimental_realtime_start_instructions = "start instructions from config" @@ -6437,7 +6515,8 @@ experimental_realtime_start_instructions = "start instructions from config" cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_start_instructions.as_deref(), @@ -6446,8 +6525,8 @@ experimental_realtime_start_instructions = "start instructions from config" Ok(()) } -#[test] -fn experimental_realtime_ws_base_url_loads_from_config_toml() -> std::io::Result<()> { +#[tokio::test] +async fn experimental_realtime_ws_base_url_loads_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" experimental_realtime_ws_base_url = "http://127.0.0.1:8011" @@ -6465,7 +6544,8 @@ experimental_realtime_ws_base_url = "http://127.0.0.1:8011" cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_ws_base_url.as_deref(), @@ -6474,8 +6554,8 @@ experimental_realtime_ws_base_url = "http://127.0.0.1:8011" Ok(()) } -#[test] -fn experimental_realtime_ws_backend_prompt_loads_from_config_toml() -> std::io::Result<()> { +#[tokio::test] +async fn experimental_realtime_ws_backend_prompt_loads_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" experimental_realtime_ws_backend_prompt = "prompt from config" @@ -6493,7 +6573,8 @@ experimental_realtime_ws_backend_prompt = "prompt from config" cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_ws_backend_prompt.as_deref(), @@ -6502,8 +6583,8 @@ experimental_realtime_ws_backend_prompt = "prompt from config" Ok(()) } -#[test] -fn experimental_realtime_ws_startup_context_loads_from_config_toml() -> std::io::Result<()> { +#[tokio::test] +async fn experimental_realtime_ws_startup_context_loads_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" experimental_realtime_ws_startup_context = "startup context from config" @@ -6521,7 +6602,8 @@ experimental_realtime_ws_startup_context = "startup context from config" cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_ws_startup_context.as_deref(), @@ -6530,8 +6612,8 @@ experimental_realtime_ws_startup_context = "startup context from config" Ok(()) } -#[test] -fn experimental_realtime_ws_model_loads_from_config_toml() -> std::io::Result<()> { +#[tokio::test] +async fn experimental_realtime_ws_model_loads_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" experimental_realtime_ws_model = "realtime-test-model" @@ -6549,7 +6631,8 @@ experimental_realtime_ws_model = "realtime-test-model" cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_ws_model.as_deref(), @@ -6558,8 +6641,8 @@ experimental_realtime_ws_model = "realtime-test-model" Ok(()) } -#[test] -fn realtime_config_partial_table_uses_realtime_defaults() -> std::io::Result<()> { +#[tokio::test] +async fn realtime_config_partial_table_uses_realtime_defaults() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" [realtime] @@ -6573,7 +6656,8 @@ voice = "marin" cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.realtime, @@ -6585,8 +6669,8 @@ voice = "marin" Ok(()) } -#[test] -fn realtime_loads_from_config_toml() -> std::io::Result<()> { +#[tokio::test] +async fn realtime_loads_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" [realtime] @@ -6613,7 +6697,8 @@ voice = "cedar" cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!( config.realtime, @@ -6627,8 +6712,8 @@ voice = "cedar" Ok(()) } -#[test] -fn realtime_audio_loads_from_config_toml() -> std::io::Result<()> { +#[tokio::test] +async fn realtime_audio_loads_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" [audio] @@ -6650,7 +6735,8 @@ speaker = "Desk Speakers" cfg, ConfigOverrides::default(), codex_home.abs(), - )?; + ) + .await?; assert_eq!(config.realtime_audio.microphone.as_deref(), Some("USB Mic")); assert_eq!( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 5faf4be480..adfbe45baa 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -169,13 +169,14 @@ fn resolve_mcp_oauth_credentials_store_mode( } #[cfg(test)] -pub(crate) fn test_config() -> Config { +pub(crate) async fn test_config() -> Config { let codex_home = tempfile::tempdir().expect("create temp dir"); Config::load_from_base_config_with_overrides( ConfigToml::default(), ConfigOverrides::default(), AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("temp dir should resolve"), ) + .await .expect("load default test config") } @@ -726,6 +727,7 @@ impl ConfigBuilder { codex_home, config_layer_stack, ) + .await } #[cfg(test)] @@ -786,7 +788,7 @@ impl Config { } /// Load a default configuration when user config files are invalid. - pub fn load_default_with_cli_overrides( + pub async fn load_default_with_cli_overrides( cli_overrides: Vec<(String, TomlValue)>, ) -> std::io::Result { let codex_home = find_codex_home()?; @@ -794,11 +796,12 @@ impl Config { codex_home.to_path_buf(), cli_overrides, ) + .await } /// Load a default configuration for a specific Codex home without reading /// user, project, or system config layers. - pub fn load_default_with_cli_overrides_for_codex_home( + pub async fn load_default_with_cli_overrides_for_codex_home( codex_home: PathBuf, cli_overrides: Vec<(String, TomlValue)>, ) -> std::io::Result { @@ -818,6 +821,7 @@ impl Config { codex_home, ConfigLayerStack::default(), ) + .await } /// This is a secondary way of creating [Config], which is appropriate when @@ -1413,22 +1417,24 @@ pub(crate) fn resolve_web_search_mode_for_turn( impl Config { #[cfg(test)] - fn load_from_base_config_with_overrides( + async fn load_from_base_config_with_overrides( cfg: ConfigToml, overrides: ConfigOverrides, codex_home: AbsolutePathBuf, ) -> std::io::Result { // Note this ignores requirements.toml enforcement for tests. let config_layer_stack = ConfigLayerStack::default(); - Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack) + Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack).await } - pub(crate) fn load_config_with_layer_stack( + pub(crate) async fn load_config_with_layer_stack( cfg: ConfigToml, overrides: ConfigOverrides, codex_home: AbsolutePathBuf, config_layer_stack: ConfigLayerStack, ) -> std::io::Result { + // Keep the large config-construction future off small test thread stacks. + Box::pin(async move { validate_model_providers(&cfg.model_providers) .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; // Ensure that every field of ConfigRequirements is applied to the final @@ -1547,6 +1553,7 @@ impl Config { .collect(); let active_project = cfg .get_active_project(resolved_cwd.as_path()) + .await .unwrap_or(ProjectConfig { trust_level: None }); let permission_config_syntax = resolve_permission_config_syntax( &config_layer_stack, @@ -1636,13 +1643,15 @@ impl Config { ) } else { let configured_network_proxy_config = NetworkProxyConfig::default(); - let mut sandbox_policy = cfg.derive_sandbox_policy( - sandbox_mode, - config_profile.sandbox_mode, - windows_sandbox_level, - resolved_cwd.as_path(), - Some(&constrained_sandbox_policy), - ); + let mut sandbox_policy = cfg + .derive_sandbox_policy( + sandbox_mode, + config_profile.sandbox_mode, + windows_sandbox_level, + resolved_cwd.as_path(), + Some(&constrained_sandbox_policy), + ) + .await; if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy { for path in &additional_writable_roots { if !writable_roots.iter().any(|existing| existing == path) { @@ -2207,6 +2216,8 @@ impl Config { }, }; Ok(config) + }) + .await } fn load_instructions(codex_dir: Option<&AbsolutePathBuf>) -> Option { diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index 268346794c..34d22c2bfb 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -24,8 +24,8 @@ fn normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths() { assert_eq!(parsed, PathBuf::from(r"D:\c\x\worktrees\2508\swift-base")); } -#[test] -fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> { +#[tokio::test] +async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> { let temp_dir = TempDir::new()?; let cwd = temp_dir.path().join("workspace"); let codex_home = temp_dir.path().join(".codex"); @@ -64,7 +64,8 @@ fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> ..Default::default() }, AbsolutePathBuf::from_absolute_path(&codex_home)?, - )?; + ) + .await?; let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?; let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?; diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 36fc956bf4..f7830b7089 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -650,7 +650,7 @@ async fn project_trust_context( let projects = project_trust_config.projects.unwrap_or_default(); let project_root_key = project_trust_key(project_root.as_path()); - let repo_root = resolve_root_git_project_for_trust(cwd.as_path()); + let repo_root = resolve_root_git_project_for_trust(cwd.as_path()).await; let repo_root_key = repo_root.as_ref().map(|root| project_trust_key(root)); let projects_trust = projects diff --git a/codex-rs/core/src/git_info_tests.rs b/codex-rs/core/src/git_info_tests.rs index 4cb50f02fd..e3ef1fa62f 100644 --- a/codex-rs/core/src/git_info_tests.rs +++ b/codex-rs/core/src/git_info_tests.rs @@ -426,10 +426,14 @@ async fn test_get_git_working_tree_state_branch_fallback() { assert_eq!(state.sha, GitSha::new(&remote_sha)); } -#[test] -fn resolve_root_git_project_for_trust_returns_none_outside_repo() { +#[tokio::test] +async fn resolve_root_git_project_for_trust_returns_none_outside_repo() { let tmp = TempDir::new().expect("tempdir"); - assert!(resolve_root_git_project_for_trust(tmp.path()).is_none()); + assert!( + resolve_root_git_project_for_trust(tmp.path()) + .await + .is_none() + ); } #[tokio::test] @@ -439,12 +443,15 @@ async fn resolve_root_git_project_for_trust_regular_repo_returns_repo_root() { let expected = std::fs::canonicalize(&repo_path).unwrap(); assert_eq!( - resolve_root_git_project_for_trust(&repo_path), + resolve_root_git_project_for_trust(&repo_path).await, Some(expected.clone()) ); let nested = repo_path.join("sub/dir"); std::fs::create_dir_all(&nested).unwrap(); - assert_eq!(resolve_root_git_project_for_trust(&nested), Some(expected)); + assert_eq!( + resolve_root_git_project_for_trust(&nested).await, + Some(expected) + ); } #[tokio::test] @@ -467,18 +474,20 @@ async fn resolve_root_git_project_for_trust_detects_worktree_and_returns_main_ro .expect("git worktree add"); let expected = std::fs::canonicalize(&repo_path).ok(); - let got = - resolve_root_git_project_for_trust(&wt_root).and_then(|p| std::fs::canonicalize(p).ok()); + let got = resolve_root_git_project_for_trust(&wt_root) + .await + .and_then(|p| std::fs::canonicalize(p).ok()); assert_eq!(got, expected); let nested = wt_root.join("nested/sub"); std::fs::create_dir_all(&nested).unwrap(); - let got_nested = - resolve_root_git_project_for_trust(&nested).and_then(|p| std::fs::canonicalize(p).ok()); + let got_nested = resolve_root_git_project_for_trust(&nested) + .await + .and_then(|p| std::fs::canonicalize(p).ok()); assert_eq!(got_nested, expected); } -#[test] -fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_command() { +#[tokio::test] +async fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_command() { let tmp = TempDir::new().expect("tempdir"); let repo_root = tmp.path().join("repo"); let common_dir = repo_root.join(".git"); @@ -495,17 +504,17 @@ fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_comma let expected = std::fs::canonicalize(&repo_root).unwrap(); assert_eq!( - resolve_root_git_project_for_trust(&worktree_root), + resolve_root_git_project_for_trust(&worktree_root).await, Some(expected.clone()) ); assert_eq!( - resolve_root_git_project_for_trust(&worktree_root.join("nested")), + resolve_root_git_project_for_trust(&worktree_root.join("nested")).await, Some(expected) ); } -#[test] -fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() { +#[tokio::test] +async fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() { let tmp = TempDir::new().expect("tempdir"); let proj = tmp.path().join("proj"); std::fs::create_dir_all(proj.join("nested")).unwrap(); @@ -520,8 +529,12 @@ fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() { ) .unwrap(); - assert!(resolve_root_git_project_for_trust(&proj).is_none()); - assert!(resolve_root_git_project_for_trust(&proj.join("nested")).is_none()); + assert!(resolve_root_git_project_for_trust(&proj).await.is_none()); + assert!( + resolve_root_git_project_for_trust(&proj.join("nested")) + .await + .is_none() + ); } #[tokio::test] diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 6cb9aefa14..f1c6e1e054 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -823,9 +823,9 @@ async fn interrupt_and_drain_turn(codex: &Codex) -> anyhow::Result<()> { mod tests { use super::*; - #[test] - fn guardian_review_session_config_change_invalidates_cached_session() { - let parent_config = crate::config::test_config(); + #[tokio::test] + async fn guardian_review_session_config_change_invalidates_cached_session() { + let parent_config = crate::config::test_config().await; let cached_spawn_config = build_guardian_review_session_config( &parent_config, /*live_network_config*/ None, @@ -855,9 +855,9 @@ mod tests { ); } - #[test] - fn guardian_review_session_config_disables_hooks() { - let mut parent_config = crate::config::test_config(); + #[tokio::test] + async fn guardian_review_session_config_disables_hooks() { + let mut parent_config = crate::config::test_config().await; parent_config .features .enable(Feature::CodexHooks) diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 8940753d27..485f222551 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -1332,8 +1332,8 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() -> Ok(()) } -#[test] -fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow::Result<()> { +#[tokio::test] +async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow::Result<()> { const TEST_STACK_SIZE_BYTES: usize = 2 * 1024 * 1024; let handle = @@ -1569,9 +1569,9 @@ fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow: )), } } -#[test] -fn guardian_review_session_config_preserves_parent_network_proxy() { - let mut parent_config = test_config(); +#[tokio::test] +async fn guardian_review_session_config_preserves_parent_network_proxy() { + let mut parent_config = test_config().await; let network = NetworkProxySpec::from_config_and_constraints( NetworkProxyConfig::default(), Some(NetworkConstraints { @@ -1616,9 +1616,9 @@ fn guardian_review_session_config_preserves_parent_network_proxy() { ); } -#[test] -fn guardian_review_session_config_overrides_parent_developer_instructions() { - let mut parent_config = test_config(); +#[tokio::test] +async fn guardian_review_session_config_overrides_parent_developer_instructions() { + let mut parent_config = test_config().await; parent_config.developer_instructions = Some("parent or managed config should not replace guardian policy".to_string()); @@ -1636,9 +1636,9 @@ fn guardian_review_session_config_overrides_parent_developer_instructions() { ); } -#[test] -fn guardian_review_session_config_uses_live_network_proxy_state() { - let mut parent_config = test_config(); +#[tokio::test] +async fn guardian_review_session_config_uses_live_network_proxy_state() { + let mut parent_config = test_config().await; let mut parent_network = NetworkProxyConfig::default(); parent_network.network.enabled = true; parent_network @@ -1680,9 +1680,9 @@ fn guardian_review_session_config_uses_live_network_proxy_state() { ); } -#[test] -fn guardian_review_session_config_rejects_pinned_collab_feature() { - let mut parent_config = test_config(); +#[tokio::test] +async fn guardian_review_session_config_rejects_pinned_collab_feature() { + let mut parent_config = test_config().await; parent_config.features = ManagedFeatures::from_configured( parent_config.features.get().clone(), Some(Sourced { @@ -1708,9 +1708,9 @@ fn guardian_review_session_config_rejects_pinned_collab_feature() { ); } -#[test] -fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_slug() { - let mut parent_config = test_config(); +#[tokio::test] +async fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_slug() { + let mut parent_config = test_config().await; parent_config.model = Some("configured-model".to_string()); let guardian_config = build_guardian_review_session_config_for_test( @@ -1724,8 +1724,8 @@ fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_ assert_eq!(guardian_config.model, Some("active-model".to_string())); } -#[test] -fn guardian_review_session_config_uses_requirements_guardian_policy_config() { +#[tokio::test] +async fn guardian_review_session_config_uses_requirements_guardian_policy_config() { let codex_home = tempfile::tempdir().expect("create temp dir"); let workspace = tempfile::tempdir().expect("create temp dir"); let config_layer_stack = ConfigLayerStack::new( @@ -1748,6 +1748,7 @@ fn guardian_review_session_config_uses_requirements_guardian_policy_config() { codex_home.abs(), config_layer_stack, ) + .await .expect("load config"); let guardian_config = build_guardian_review_session_config_for_test( @@ -1766,8 +1767,9 @@ fn guardian_review_session_config_uses_requirements_guardian_policy_config() { ); } -#[test] -fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override() { +#[tokio::test] +async fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override() +{ let codex_home = tempfile::tempdir().expect("create temp dir"); let workspace = tempfile::tempdir().expect("create temp dir"); let config_layer_stack = @@ -1782,6 +1784,7 @@ fn guardian_review_session_config_uses_default_guardian_policy_without_requireme codex_home.abs(), config_layer_stack, ) + .await .expect("load config"); let guardian_config = build_guardian_review_session_config_for_test( diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index 71809d382d..9959b1fcb5 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -468,7 +468,7 @@ mod phase2 { impl DispatchHarness { async fn new() -> Self { let codex_home = tempfile::tempdir().expect("create temp codex home"); - let mut config = test_config(); + let mut config = test_config().await; config.codex_home = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path()) .expect("codex home is absolute"); @@ -895,7 +895,7 @@ mod phase2 { #[tokio::test] async fn dispatch_marks_job_for_retry_when_spawn_agent_fails() { let codex_home = tempfile::tempdir().expect("create temp codex home"); - let mut config = test_config(); + let mut config = test_config().await; config.codex_home = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path()) .expect("codex home is absolute"); diff --git a/codex-rs/core/src/prompt_debug.rs b/codex-rs/core/src/prompt_debug.rs index d5fa1863f2..fb64481523 100644 --- a/codex-rs/core/src/prompt_debug.rs +++ b/codex-rs/core/src/prompt_debug.rs @@ -106,7 +106,7 @@ mod tests { async fn build_prompt_input_includes_context_and_user_message() { let codex_home = tempfile::tempdir().expect("create codex home"); let cwd = tempfile::tempdir().expect("create cwd"); - let mut config = test_config(); + let mut config = test_config().await; config.codex_home = AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("codex home is absolute"); config.cwd = AbsolutePathBuf::try_from(cwd.path().to_path_buf()).expect("absolute cwd"); diff --git a/codex-rs/core/src/realtime_context.rs b/codex-rs/core/src/realtime_context.rs index cac2067a82..fc17dab99b 100644 --- a/codex-rs/core/src/realtime_context.rs +++ b/codex-rs/core/src/realtime_context.rs @@ -62,8 +62,8 @@ pub(crate) async fn build_realtime_startup_context( let history = sess.clone_history().await; let current_thread_section = build_current_thread_section(history.raw_items()); let recent_threads = load_recent_threads(sess).await; - let recent_work_section = build_recent_work_section(&cwd, &recent_threads); - let workspace_section = build_workspace_section_with_user_root(&cwd, home_dir()); + let recent_work_section = build_recent_work_section(&cwd, &recent_threads).await; + let workspace_section = build_workspace_section_with_user_root(&cwd, home_dir()).await; if current_thread_section.is_none() && recent_work_section.is_none() @@ -144,16 +144,18 @@ async fn load_recent_threads(sess: &Session) -> Vec { } } -fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Option { +async fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Option { let mut groups: HashMap> = HashMap::new(); for entry in recent_threads { - let group = - resolve_root_git_project_for_trust(&entry.cwd).unwrap_or_else(|| entry.cwd.clone()); + let group = resolve_root_git_project_for_trust(&entry.cwd) + .await + .unwrap_or_else(|| entry.cwd.clone()); groups.entry(group).or_default().push(entry); } - let current_group = - resolve_root_git_project_for_trust(cwd).unwrap_or_else(|| cwd.to_path_buf()); + let current_group = resolve_root_git_project_for_trust(cwd) + .await + .unwrap_or_else(|| cwd.to_path_buf()); let mut groups = groups.into_iter().collect::>(); groups.sort_by(|(left_group, left_entries), (right_group, right_entries)| { let left_latest = left_entries @@ -178,14 +180,13 @@ fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Opt )) }); - let sections = groups - .into_iter() - .take(MAX_RECENT_WORK_GROUPS) - .filter_map(|(group, mut entries)| { - entries.sort_by_key(|entry| Reverse(entry.updated_at)); - format_thread_group(¤t_group, &group, entries) - }) - .collect::>(); + let mut sections = Vec::new(); + for (group, mut entries) in groups.into_iter().take(MAX_RECENT_WORK_GROUPS) { + entries.sort_by_key(|entry| Reverse(entry.updated_at)); + if let Some(section) = format_thread_group(¤t_group, &group, entries).await { + sections.push(section); + } + } (!sections.is_empty()).then(|| sections.join("\n\n")) } @@ -306,11 +307,11 @@ pub(crate) fn truncate_realtime_text_to_token_budget(text: &str, budget_tokens: } } -fn build_workspace_section_with_user_root( +async fn build_workspace_section_with_user_root( cwd: &Path, user_root: Option, ) -> Option { - let git_root = resolve_root_git_project_for_trust(cwd); + let git_root = resolve_root_git_project_for_trust(cwd).await; let cwd_tree = render_tree(cwd); let git_root_tree = git_root .as_ref() @@ -466,13 +467,16 @@ fn format_startup_context_blob(body: &str, budget_tokens: usize) -> String { } } -fn format_thread_group( +async fn format_thread_group( current_group: &Path, group: &Path, entries: Vec<&StoredThread>, ) -> Option { let latest = entries.first()?; - let group_label = if resolve_root_git_project_for_trust(latest.cwd.as_path()).is_some() { + let group_label = if resolve_root_git_project_for_trust(latest.cwd.as_path()) + .await + .is_some() + { format!("### Git repo: {}", group.display()) } else { format!("### Directory: {}", group.display()) diff --git a/codex-rs/core/src/realtime_context_tests.rs b/codex-rs/core/src/realtime_context_tests.rs index 7fe438dac7..4f62affcae 100644 --- a/codex-rs/core/src/realtime_context_tests.rs +++ b/codex-rs/core/src/realtime_context_tests.rs @@ -188,30 +188,31 @@ fn startup_context_blob_is_wrapped_in_tags_and_fits_budget() { assert!(wrapped.len().div_ceil(4) <= 200); } -#[test] -fn workspace_section_requires_meaningful_structure() { +#[tokio::test] +async fn workspace_section_requires_meaningful_structure() { let cwd = TempDir::new().expect("tempdir"); assert_eq!( - build_workspace_section_with_user_root(cwd.path(), /*user_root*/ None), + build_workspace_section_with_user_root(cwd.path(), /*user_root*/ None).await, None ); } -#[test] -fn workspace_section_includes_tree_when_entries_exist() { +#[tokio::test] +async fn workspace_section_includes_tree_when_entries_exist() { let cwd = TempDir::new().expect("tempdir"); fs::create_dir(cwd.path().join("docs")).expect("create docs dir"); fs::write(cwd.path().join("README.md"), "hello").expect("write readme"); let section = build_workspace_section_with_user_root(cwd.path(), /*user_root*/ None) + .await .expect("workspace section"); assert!(section.contains("Working directory tree:")); assert!(section.contains("- docs/")); assert!(section.contains("- README.md")); } -#[test] -fn workspace_section_includes_user_root_tree_when_distinct() { +#[tokio::test] +async fn workspace_section_includes_user_root_tree_when_distinct() { let root = TempDir::new().expect("tempdir"); let cwd = root.path().join("cwd"); let git_root = root.path().join("git"); @@ -225,14 +226,15 @@ fn workspace_section_includes_user_root_tree_when_distinct() { fs::write(user_root.join(".zshrc"), "export TEST=1").expect("write home file"); let section = build_workspace_section_with_user_root(cwd.as_path(), Some(user_root)) + .await .expect("workspace section"); assert!(section.contains("User root tree:")); assert!(section.contains("- code/")); assert!(!section.contains("- .zshrc")); } -#[test] -fn recent_work_section_groups_threads_by_cwd() { +#[tokio::test] +async fn recent_work_section_groups_threads_by_cwd() { let root = TempDir::new().expect("tempdir"); let repo = root.path().join("repo"); let workspace_a = repo.join("workspace-a"); @@ -268,6 +270,7 @@ fn recent_work_section_groups_threads_by_cwd() { let repo = fs::canonicalize(repo).expect("canonicalize repo"); let section = build_recent_work_section(current_cwd.as_path(), &recent_threads) + .await .expect("recent work section"); assert!(section.contains(&format!("### Git repo: {}", repo.display()))); assert!(section.contains("Recent sessions: 2")); diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 57d6ba8d6d..06f89fc728 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -237,7 +237,7 @@ async fn ignores_session_prefix_messages_when_truncating() { #[tokio::test] async fn shutdown_all_threads_bounded_submits_shutdown_to_every_thread() { let temp_dir = tempdir().expect("tempdir"); - let mut config = test_config(); + let mut config = test_config().await; config.codex_home = temp_dir.path().join("codex-home").abs(); config.cwd = config.codex_home.abs(); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); @@ -279,7 +279,7 @@ async fn new_uses_configured_openai_provider_for_model_refresh() { let models_mock = mount_models_once(&server, ModelsResponse { models: vec![] }).await; let temp_dir = tempdir().expect("tempdir"); - let mut config = test_config(); + let mut config = test_config().await; config.codex_home = temp_dir.path().join("codex-home").abs(); config.cwd = config.codex_home.abs(); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); @@ -422,7 +422,7 @@ fn mixed_response_and_legacy_user_event_history_is_mid_turn() { #[tokio::test] async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_history() { let temp_dir = tempdir().expect("tempdir"); - let mut config = test_config(); + let mut config = test_config().await; config.codex_home = temp_dir.path().join("codex-home").abs(); config.cwd = config.codex_home.abs(); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); @@ -525,7 +525,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor #[tokio::test] async fn interrupted_fork_snapshot_preserves_explicit_turn_id() { let temp_dir = tempdir().expect("tempdir"); - let mut config = test_config(); + let mut config = test_config().await; config.codex_home = temp_dir.path().join("codex-home").abs(); config.cwd = config.codex_home.abs(); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); @@ -618,7 +618,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() { #[tokio::test] async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_source() { let temp_dir = tempdir().expect("tempdir"); - let mut config = test_config(); + let mut config = test_config().await; config.codex_home = temp_dir.path().join("codex-home").abs(); config.cwd = config.codex_home.abs(); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index cab38bc21c..fa52aa6b0f 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -107,8 +107,8 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab })) } -fn search_capable_model_info() -> ModelInfo { - let config = test_config(); +async fn search_capable_model_info() -> ModelInfo { + let config = test_config().await; let mut model_info = construct_model_info_offline("gpt-5-codex", &config); model_info.supports_search_tool = true; model_info @@ -216,8 +216,8 @@ fn find_namespace_function_tool<'a>( .unwrap_or_else(|| panic!("expected tool {expected_namespace}{expected_name} in namespace")) } -fn multi_agent_v2_tools_config() -> ToolsConfig { - let config = test_config(); +async fn multi_agent_v2_tools_config() -> ToolsConfig { + let config = test_config().await; let model_info = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); @@ -250,8 +250,8 @@ fn multi_agent_v2_spawn_agent_description(tools_config: &ToolsConfig) -> String description.clone() } -fn model_info_from_models_json(slug: &str) -> ModelInfo { - let config = test_config(); +async fn model_info_from_models_json(slug: &str) -> ModelInfo { + let config = test_config().await; let response = bundled_models_response() .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let model = response @@ -295,9 +295,9 @@ fn build_specs_with_unavailable_tools( ) } -#[test] -fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() { - let mut model_info = model_info_from_models_json("gpt-5-codex"); +#[tokio::test] +async fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() { + let mut model_info = model_info_from_models_json("gpt-5-codex").await; model_info.shell_type = ConfigShellToolType::UnifiedExec; let features = Features::with_defaults(); let available_models = Vec::new(); @@ -320,9 +320,9 @@ fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() { assert_eq!(config.shell_type, expected_shell_type); } -#[test] -fn get_memory_requires_feature_flag() { - let config = test_config(); +#[tokio::test] +async fn get_memory_requires_feature_flag() { + let config = test_config().await; let model_info = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.disable(Feature::MemoryTool); @@ -350,14 +350,14 @@ fn get_memory_requires_feature_flag() { ); } -fn assert_model_tools( +async fn assert_model_tools( model_slug: &str, features: &Features, web_search_mode: Option, expected_tools: &[&str], ) { - let _config = test_config(); - let model_info = model_info_from_models_json(model_slug); + let _config = test_config().await; + let model_info = model_info_from_models_json(model_slug).await; let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -388,7 +388,7 @@ fn assert_model_tools( assert_eq!(&tool_names, &expected_tools,); } -fn assert_default_model_tools( +async fn assert_default_model_tools( model_slug: &str, features: &Features, web_search_mode: Option, @@ -401,11 +401,11 @@ fn assert_default_model_tools( vec![shell_tool] }; expected.extend(expected_tail); - assert_model_tools(model_slug, features, web_search_mode, &expected); + assert_model_tools(model_slug, features, web_search_mode, &expected).await; } -#[test] -fn test_build_specs_gpt5_codex_default() { +#[tokio::test] +async fn test_build_specs_gpt5_codex_default() { let features = Features::with_defaults(); assert_default_model_tools( "gpt-5-codex", @@ -424,11 +424,12 @@ fn test_build_specs_gpt5_codex_default() { "wait_agent", "close_agent", ], - ); + ) + .await; } -#[test] -fn test_build_specs_gpt51_codex_default() { +#[tokio::test] +async fn test_build_specs_gpt51_codex_default() { let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex", @@ -447,11 +448,12 @@ fn test_build_specs_gpt51_codex_default() { "wait_agent", "close_agent", ], - ); + ) + .await; } -#[test] -fn test_build_specs_gpt5_codex_unified_exec_web_search() { +#[tokio::test] +async fn test_build_specs_gpt5_codex_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); assert_model_tools( @@ -472,11 +474,12 @@ fn test_build_specs_gpt5_codex_unified_exec_web_search() { "wait_agent", "close_agent", ], - ); + ) + .await; } -#[test] -fn test_build_specs_gpt51_codex_unified_exec_web_search() { +#[tokio::test] +async fn test_build_specs_gpt51_codex_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); assert_model_tools( @@ -497,11 +500,12 @@ fn test_build_specs_gpt51_codex_unified_exec_web_search() { "wait_agent", "close_agent", ], - ); + ) + .await; } -#[test] -fn test_gpt_5_1_codex_max_defaults() { +#[tokio::test] +async fn test_gpt_5_1_codex_max_defaults() { let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex-max", @@ -520,11 +524,12 @@ fn test_gpt_5_1_codex_max_defaults() { "wait_agent", "close_agent", ], - ); + ) + .await; } -#[test] -fn test_codex_5_1_mini_defaults() { +#[tokio::test] +async fn test_codex_5_1_mini_defaults() { let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex-mini", @@ -543,11 +548,12 @@ fn test_codex_5_1_mini_defaults() { "wait_agent", "close_agent", ], - ); + ) + .await; } -#[test] -fn test_gpt_5_defaults() { +#[tokio::test] +async fn test_gpt_5_defaults() { let features = Features::with_defaults(); assert_default_model_tools( "gpt-5", @@ -565,11 +571,12 @@ fn test_gpt_5_defaults() { "wait_agent", "close_agent", ], - ); + ) + .await; } -#[test] -fn test_gpt_5_1_defaults() { +#[tokio::test] +async fn test_gpt_5_1_defaults() { let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1", @@ -588,11 +595,12 @@ fn test_gpt_5_1_defaults() { "wait_agent", "close_agent", ], - ); + ) + .await; } -#[test] -fn test_gpt_5_1_codex_max_unified_exec_web_search() { +#[tokio::test] +async fn test_gpt_5_1_codex_max_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); assert_model_tools( @@ -613,12 +621,13 @@ fn test_gpt_5_1_codex_max_unified_exec_web_search() { "wait_agent", "close_agent", ], - ); + ) + .await; } -#[test] -fn test_build_specs_default_shell_present() { - let config = test_config(); +#[tokio::test] +async fn test_build_specs_default_shell_present() { + let config = test_config().await; let model_info = construct_model_info_offline("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -649,9 +658,9 @@ fn test_build_specs_default_shell_present() { assert_contains_tool_names(&tools, &subset); } -#[test] -fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { - let config = test_config(); +#[tokio::test] +async fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { + let config = test_config().await; let model_info = construct_model_info_offline("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -713,9 +722,10 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { ); } -#[test] -fn spawn_agent_description_omits_usage_hint_when_disabled() { +#[tokio::test] +async fn spawn_agent_description_omits_usage_hint_when_disabled() { let tools_config = multi_agent_v2_tools_config() + .await .with_spawn_agent_usage_hint(/*spawn_agent_usage_hint*/ false); let description = multi_agent_v2_spawn_agent_description(&tools_config); @@ -734,11 +744,13 @@ fn spawn_agent_description_omits_usage_hint_when_disabled() { ); } -#[test] -fn spawn_agent_description_uses_configured_usage_hint_text() { - let tools_config = multi_agent_v2_tools_config().with_spawn_agent_usage_hint_text(Some( - /*spawn_agent_usage_hint_text*/ "Custom delegation guidance only.".to_string(), - )); +#[tokio::test] +async fn spawn_agent_description_uses_configured_usage_hint_text() { + let tools_config = multi_agent_v2_tools_config() + .await + .with_spawn_agent_usage_hint_text(Some( + /*spawn_agent_usage_hint_text*/ "Custom delegation guidance only.".to_string(), + )); let description = multi_agent_v2_spawn_agent_description(&tools_config); assert_regex_match( @@ -757,9 +769,9 @@ fn spawn_agent_description_uses_configured_usage_hint_text() { ); } -#[test] -fn tool_suggest_requires_apps_and_plugins_features() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn tool_suggest_requires_apps_and_plugins_features() { + let model_info = search_capable_model_info().await; let discoverable_tools = Some(vec![discoverable_connector( "connector_2128aebfecb84f64a069897515042a44", "Google Calendar", @@ -804,9 +816,9 @@ fn tool_suggest_requires_apps_and_plugins_features() { } } -#[test] -fn search_tool_description_handles_no_enabled_mcp_tools() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn search_tool_description_handles_no_enabled_mcp_tools() { + let model_info = search_capable_model_info().await; let mut features = Features::with_defaults(); features.enable(Feature::Apps); features.enable(Feature::ToolSearch); @@ -838,9 +850,9 @@ fn search_tool_description_handles_no_enabled_mcp_tools() { assert!(!description.contains("{{source_descriptions}}")); } -#[test] -fn search_tool_description_falls_back_to_connector_name_without_description() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn search_tool_description_falls_back_to_connector_name_without_description() { + let model_info = search_capable_model_info().await; let mut features = Features::with_defaults(); features.enable(Feature::Apps); features.enable(Feature::ToolSearch); @@ -889,9 +901,9 @@ fn search_tool_description_falls_back_to_connector_name_without_description() { assert!(!description.contains("- Calendar:")); } -#[test] -fn search_tool_registers_namespaced_mcp_tool_aliases() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn search_tool_registers_namespaced_mcp_tool_aliases() { + let model_info = search_capable_model_info().await; let mut features = Features::with_defaults(); features.enable(Feature::Apps); features.enable(Feature::ToolSearch); @@ -974,9 +986,9 @@ fn search_tool_registers_namespaced_mcp_tool_aliases() { assert!(registry.has_handler(&mcp_alias)); } -#[test] -fn direct_mcp_tools_register_namespaced_handlers() { - let config = test_config(); +#[tokio::test] +async fn direct_mcp_tools_register_namespaced_handlers() { + let config = test_config().await; let model_info = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1011,9 +1023,9 @@ fn direct_mcp_tools_register_namespaced_handlers() { assert!(!registry.has_handler(&ToolName::plain("mcp__test_server__echo"))); } -#[test] -fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() { - let config = test_config(); +#[tokio::test] +async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() { + let config = test_config().await; let model_info = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1060,9 +1072,9 @@ fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() { assert!(!registry.has_handler(&ToolName::plain("mcp__codex_apps__calendar_create_event"))); } -#[test] -fn test_mcp_tool_property_missing_type_defaults_to_string() { - let config = test_config(); +#[tokio::test] +async fn test_mcp_tool_property_missing_type_defaults_to_string() { + let config = test_config().await; let model_info = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1123,9 +1135,9 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() { ); } -#[test] -fn test_mcp_tool_preserves_integer_schema() { - let config = test_config(); +#[tokio::test] +async fn test_mcp_tool_preserves_integer_schema() { + let config = test_config().await; let model_info = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1184,9 +1196,9 @@ fn test_mcp_tool_preserves_integer_schema() { ); } -#[test] -fn test_mcp_tool_array_without_items_gets_default_string_items() { - let config = test_config(); +#[tokio::test] +async fn test_mcp_tool_array_without_items_gets_default_string_items() { + let config = test_config().await; let model_info = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1249,9 +1261,9 @@ fn test_mcp_tool_array_without_items_gets_default_string_items() { ); } -#[test] -fn test_mcp_tool_anyof_defaults_to_string() { - let config = test_config(); +#[tokio::test] +async fn test_mcp_tool_anyof_defaults_to_string() { + let config = test_config().await; let model_info = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1318,9 +1330,9 @@ fn test_mcp_tool_anyof_defaults_to_string() { ); } -#[test] -fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { - let config = test_config(); +#[tokio::test] +async fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { + let config = test_config().await; let model_info = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1433,8 +1445,8 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { ); } -#[test] -fn code_mode_only_restricts_model_tools_to_exec_tools() { +#[tokio::test] +async fn code_mode_only_restricts_model_tools_to_exec_tools() { let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); features.enable(Feature::CodeModeOnly); @@ -1444,5 +1456,6 @@ fn code_mode_only_restricts_model_tools_to_exec_tools() { &features, Some(WebSearchMode::Live), &["exec", "wait"], - ); + ) + .await; } diff --git a/codex-rs/git-utils/src/info.rs b/codex-rs/git-utils/src/info.rs index 048a8cedb6..e28a0bae42 100644 --- a/codex-rs/git-utils/src/info.rs +++ b/codex-rs/git-utils/src/info.rs @@ -618,7 +618,7 @@ async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option { /// `[get_git_repo_root]`, but resolves to the root of the main /// repository. Handles worktrees via filesystem inspection without invoking /// the `git` executable. -pub fn resolve_root_git_project_for_trust(cwd: &Path) -> Option { +pub async fn resolve_root_git_project_for_trust(cwd: &Path) -> Option { let base = if cwd.is_dir() { cwd } else { cwd.parent()? }; let (repo_root, dot_git) = find_ancestor_git_entry(base)?; if dot_git.is_dir() { diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 0ae51c4ff8..c0f881e2b8 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -10,6 +10,7 @@ pub(super) async fn test_config() -> Config { .keep(); let mut config = Config::load_default_with_cli_overrides_for_codex_home(codex_home.clone(), Vec::new()) + .await .expect("config"); config.codex_home = codex_home.abs(); config.sqlite_home = codex_home.clone(); diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index 22a444cdaf..5e2e0edbde 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -4,6 +4,7 @@ use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; use codex_app_server_client::AppServerEvent; use codex_app_server_client::AppServerRequestHandle; use codex_app_server_protocol::ServerNotification; +use codex_git_utils::resolve_root_git_project_for_trust; #[cfg(target_os = "windows")] use codex_protocol::config_types::WindowsSandboxLevel; use crossterm::event::KeyCode; @@ -77,7 +78,7 @@ pub(crate) struct OnboardingResult { } impl OnboardingScreen { - pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self { + pub(crate) async fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self { let OnboardingScreenArgs { show_trust_screen, show_login_screen, @@ -122,8 +123,12 @@ impl OnboardingScreen { let show_windows_create_sandbox_hint = false; let highlighted = TrustDirectorySelection::Trust; if show_trust_screen { + let trust_target = resolve_root_git_project_for_trust(&cwd) + .await + .unwrap_or_else(|| cwd.clone()); steps.push(Step::TrustDirectory(TrustDirectoryWidget { cwd, + trust_target, codex_home, show_windows_create_sandbox_hint, should_quit: false, @@ -452,7 +457,7 @@ pub(crate) async fn run_onboarding_app( ) -> Result { use tokio_stream::StreamExt; - let mut onboarding_screen = OnboardingScreen::new(tui, args); + let mut onboarding_screen = OnboardingScreen::new(tui, args).await; // One-time guard to fully clear the screen after ChatGPT login success message is shown let mut did_full_clear_after_success = false; diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs index 23ee624627..e9f2cd58bd 100644 --- a/codex-rs/tui/src/onboarding/trust_directory.rs +++ b/codex-rs/tui/src/onboarding/trust_directory.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use crate::legacy_core::config::set_project_trust_level; -use codex_git_utils::resolve_root_git_project_for_trust; use codex_protocol::config_types::TrustLevel; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -27,6 +26,7 @@ use super::onboarding_screen::StepState; pub(crate) struct TrustDirectoryWidget { pub codex_home: PathBuf, pub cwd: PathBuf, + pub trust_target: PathBuf, pub show_windows_create_sandbox_hint: bool, pub should_quit: bool, pub selection: Option, @@ -142,8 +142,7 @@ impl StepStateProvider for TrustDirectoryWidget { impl TrustDirectoryWidget { fn handle_trust(&mut self) { - let target = - resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone()); + let target = self.trust_target.clone(); if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) { tracing::error!("Failed to set project trusted: {e:?}"); self.error = Some(format!("Failed to set trust for {}: {e}", target.display())); @@ -182,6 +181,7 @@ mod tests { let mut widget = TrustDirectoryWidget { codex_home: codex_home.path().to_path_buf(), cwd: PathBuf::from("."), + trust_target: PathBuf::from("."), show_windows_create_sandbox_hint: false, should_quit: false, selection: None, @@ -207,6 +207,7 @@ mod tests { let widget = TrustDirectoryWidget { codex_home: codex_home.path().to_path_buf(), cwd: PathBuf::from("/workspace/project"), + trust_target: PathBuf::from("/workspace/project"), show_windows_create_sandbox_hint: false, should_quit: false, selection: None,