diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 115e9808f0..5309270d57 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -821,6 +821,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/fs_api.rs b/codex-rs/app-server/src/fs_api.rs index 1f8a32362f..f71f7e8693 100644 --- a/codex-rs/app-server/src/fs_api.rs +++ b/codex-rs/app-server/src/fs_api.rs @@ -20,8 +20,8 @@ use codex_app_server_protocol::FsWriteFileResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_exec_server::CopyOptions; use codex_exec_server::CreateDirectoryOptions; -use codex_exec_server::Environment; use codex_exec_server::ExecutorFileSystem; +use codex_exec_server::LOCAL_FS; use codex_exec_server::RemoveOptions; use std::io; use std::sync::Arc; @@ -34,7 +34,7 @@ pub(crate) struct FsApi { impl Default for FsApi { fn default() -> Self { Self { - file_system: Environment::default().get_filesystem(), + file_system: Arc::new(LOCAL_FS), } } } diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index 0826f33c67..b9695c482c 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -692,6 +692,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 f044ddd2e9..a67f68af15 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -446,12 +446,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/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs index 06c6eae1e6..bbbede875a 100644 --- a/codex-rs/core/src/agent/role.rs +++ b/codex-rs/core/src/agent/role.rs @@ -71,7 +71,8 @@ async fn apply_role_to_config_inner( role_layer_toml, preserve_current_profile, preserve_current_provider, - )?; + ) + .await?; Ok(()) } @@ -143,7 +144,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, @@ -164,7 +165,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 5d6769e0ea..ba92bbae70 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -549,7 +549,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); @@ -714,8 +714,8 @@ fn collect_explicit_app_ids_from_skill_items_skips_plain_mentions_with_skill_con assert_eq!(connector_ids, HashSet::::new()); } -#[test] -fn non_app_mcp_tools_remain_visible_without_search_selection() { +#[tokio::test] +async fn non_app_mcp_tools_remain_visible_without_search_selection() { let mcp_tools = HashMap::from([ ( "mcp__codex_apps__calendar_create_event".to_string(), @@ -746,7 +746,7 @@ fn non_app_mcp_tools_remain_visible_without_search_selection() { &explicitly_enabled_connectors, &HashMap::new(), ); - let config = test_config(); + let config = test_config().await; selected_mcp_tools.extend(filter_codex_apps_mcp_tools( &mcp_tools, &connectors, @@ -758,8 +758,8 @@ fn non_app_mcp_tools_remain_visible_without_search_selection() { assert_eq!(tool_names, vec!["mcp__rmcp__echo".to_string()]); } -#[test] -fn search_tool_selection_keeps_codex_apps_tools_without_mentions() { +#[tokio::test] +async fn search_tool_selection_keeps_codex_apps_tools_without_mentions() { let selected_tool_names = [ "mcp__codex_apps__calendar_create_event".to_string(), "mcp__rmcp__echo".to_string(), @@ -793,7 +793,7 @@ fn search_tool_selection_keeps_codex_apps_tools_without_mentions() { &explicitly_enabled_connectors, &HashMap::new(), ); - let config = test_config(); + let config = test_config().await; selected_mcp_tools.extend(filter_codex_apps_mcp_tools( &mcp_tools, &connectors, @@ -811,8 +811,8 @@ fn search_tool_selection_keeps_codex_apps_tools_without_mentions() { ); } -#[test] -fn apps_mentions_add_codex_apps_tools_to_search_selected_set() { +#[tokio::test] +async fn apps_mentions_add_codex_apps_tools_to_search_selected_set() { let selected_tool_names = ["mcp__rmcp__echo".to_string()]; let mcp_tools = HashMap::from([ ( @@ -843,7 +843,7 @@ fn apps_mentions_add_codex_apps_tools_to_search_selected_set() { &explicitly_enabled_connectors, &HashMap::new(), ); - let config = test_config(); + let config = test_config().await; selected_mcp_tools.extend(filter_codex_apps_mcp_tools( &mcp_tools, &connectors, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 7d3af80249..ff75169203 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -77,8 +77,8 @@ fn http_mcp(url: &str) -> McpServerConfig { } } -#[test] -fn test_toml_parsing() { +#[tokio::test] +async fn test_toml_parsing() { let history_with_persistence = r#" [history] persistence = "save-all" @@ -144,6 +144,7 @@ consolidation_model = "gpt-5" ConfigOverrides::default(), tempdir().expect("tempdir").path().to_path_buf(), ) + .await .expect("load config from memories settings"); assert_eq!( config.memories, @@ -250,13 +251,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").path().to_path_buf(), ) + .await .expect("load config"); assert_eq!( @@ -330,8 +332,9 @@ allowed_domains = ["openai.com"] ); } -#[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")?; @@ -365,7 +368,8 @@ fn permissions_profiles_network_populates_runtime_network_proxy_spec() -> std::i ..Default::default() }, codex_home.path().to_path_buf(), - )?; + ) + .await?; let network = config .permissions .network @@ -377,8 +381,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")?; @@ -410,14 +415,15 @@ fn permissions_profiles_network_disabled_by_default_does_not_start_proxy() -> st ..Default::default() }, codex_home.path().to_path_buf(), - )?; + ) + .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"))?; @@ -458,7 +464,8 @@ fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Re ..Default::default() }, codex_home.path().to_path_buf(), - )?; + ) + .await?; let memories_root = AbsolutePathBuf::try_from(codex_home.path().join("memories")).unwrap(); assert_eq!( @@ -512,8 +519,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")?; @@ -542,6 +549,7 @@ fn permissions_profiles_require_default_permissions() -> std::io::Result<()> { }, codex_home.path().to_path_buf(), ) + .await .expect_err("missing default_permissions should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); @@ -552,8 +560,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")?; @@ -584,6 +592,7 @@ fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Resul }, codex_home.path().to_path_buf(), ) + .await .expect_err("writes outside the workspace root should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); @@ -595,8 +604,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")?; @@ -629,6 +638,7 @@ fn permissions_profiles_reject_nested_entries_for_non_project_roots() -> std::io }, codex_home.path().to_path_buf(), ) + .await .expect_err("nested entries outside :project_roots should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); @@ -639,7 +649,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")?; @@ -658,10 +670,11 @@ fn load_workspace_permission_profile(profile: PermissionProfileToml) -> std::io: }, codex_home.path().to_path_buf(), ) + .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([( @@ -670,7 +683,8 @@ fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> { )]), }), network: None, - })?; + }) + .await?; assert_eq!( config.permissions.file_system_sandbox_policy, @@ -701,8 +715,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([( @@ -714,7 +729,8 @@ fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() -> std )]), }), network: None, - })?; + }) + .await?; assert_eq!( config.permissions.file_system_sandbox_policy, @@ -735,12 +751,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, @@ -766,14 +783,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, @@ -789,8 +807,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")?; @@ -823,6 +841,7 @@ fn permissions_profiles_reject_project_root_parent_traversal() -> std::io::Resul }, codex_home.path().to_path_buf(), ) + .await .expect_err("parent traversal should be rejected for project root subpaths"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); @@ -833,8 +852,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")?; @@ -866,7 +885,8 @@ fn permissions_profiles_allow_network_enablement() -> std::io::Result<()> { ..Default::default() }, codex_home.path().to_path_buf(), - )?; + ) + .await?; assert!( config.permissions.network_sandbox_policy.is_enabled(), @@ -930,8 +950,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" @@ -941,13 +961,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, - None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - None, - ); + let resolution = sandbox_full_access_cfg + .derive_sandbox_policy( + sandbox_mode_override, + None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + None, + ) + .await; assert_eq!(resolution, SandboxPolicy::DangerFullAccess); let sandbox_read_only = r#" @@ -960,13 +982,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, - None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - None, - ); + let resolution = sandbox_read_only_cfg + .derive_sandbox_policy( + sandbox_mode_override, + None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + None, + ) + .await; assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); let writable_root = test_absolute_path("/my/workspace"); @@ -987,13 +1011,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, - None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - None, - ); + let resolution = sandbox_workspace_write_cfg + .derive_sandbox_policy( + sandbox_mode_override, + None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + None, + ) + .await; if cfg!(target_os = "windows") { assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); } else { @@ -1029,13 +1055,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, - None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - None, - ); + let resolution = sandbox_workspace_write_cfg + .derive_sandbox_policy( + sandbox_mode_override, + None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + None, + ) + .await; if cfg!(target_os = "windows") { assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); } else { @@ -1052,8 +1080,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"); @@ -1095,7 +1123,8 @@ exclude_slash_tmp = true ..Default::default() }, codex_home.path().to_path_buf(), - )?; + ) + .await?; let sandbox_policy = config.permissions.sandbox_policy.get(); assert_eq!( @@ -1256,8 +1285,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"); @@ -1275,7 +1304,8 @@ fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> { ConfigToml::default(), overrides, temp_dir.path().to_path_buf(), - )?; + ) + .await?; let expected_backend = AbsolutePathBuf::try_from(backend).unwrap(); if cfg!(target_os = "windows") { @@ -1303,8 +1333,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(), @@ -1313,15 +1343,16 @@ fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Result<( ..Default::default() }, codex_home.path().to_path_buf(), - )?; + ) + .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( @@ -1337,7 +1368,8 @@ fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> { ..Default::default() }, codex_home.path().to_path_buf(), - )?; + ) + .await?; if cfg!(target_os = "windows") { match config.permissions.sandbox_policy.get() { @@ -1370,8 +1402,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(); @@ -1379,7 +1411,8 @@ fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.cli_auth_credentials_store_mode, @@ -1389,8 +1422,8 @@ fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> { Ok(()) } -#[test] -fn config_honors_explicit_keyring_auth_store_mode() -> std::io::Result<()> { +#[tokio::test] +async fn config_honors_explicit_keyring_auth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { cli_auth_credentials_store: Some(AuthCredentialsStoreMode::Keyring), @@ -1401,7 +1434,8 @@ fn config_honors_explicit_keyring_auth_store_mode() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.cli_auth_credentials_store_mode, @@ -1411,8 +1445,8 @@ fn config_honors_explicit_keyring_auth_store_mode() -> std::io::Result<()> { Ok(()) } -#[test] -fn config_defaults_to_auto_oauth_store_mode() -> std::io::Result<()> { +#[tokio::test] +async fn config_defaults_to_auto_oauth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml::default(); @@ -1420,7 +1454,8 @@ fn config_defaults_to_auto_oauth_store_mode() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.mcp_oauth_credentials_store_mode, @@ -1430,8 +1465,8 @@ fn config_defaults_to_auto_oauth_store_mode() -> std::io::Result<()> { Ok(()) } -#[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()), @@ -1442,7 +1477,8 @@ fn feedback_enabled_defaults_to_true() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!(config.feedback_enabled, true); @@ -1582,8 +1618,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( @@ -1604,7 +1640,8 @@ fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert!(matches!( config.permissions.sandbox_policy.get(), @@ -1614,8 +1651,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( @@ -1640,7 +1677,8 @@ fn cli_override_takes_precedence_over_profile_sandbox_mode() -> std::io::Result< cfg, overrides, codex_home.path().to_path_buf(), - )?; + ) + .await?; if cfg!(target_os = "windows") { assert!(matches!( @@ -1657,8 +1695,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); @@ -1671,7 +1709,8 @@ fn feature_table_overrides_legacy_flags() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert!(!config.features.enabled(Feature::ApplyPatchFreeform)); assert!(!config.include_apply_patch_tool); @@ -1679,8 +1718,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), @@ -1692,7 +1731,8 @@ fn legacy_toggles_map_to_features() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert!(config.features.enabled(Feature::ApplyPatchFreeform)); assert!(config.features.enabled(Feature::UnifiedExec)); @@ -1704,8 +1744,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(); @@ -1719,7 +1759,8 @@ fn responses_websocket_features_do_not_change_wire_api() -> std::io::Result<()> cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.model_provider.wire_api, @@ -1730,8 +1771,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), @@ -1742,7 +1783,8 @@ fn config_honors_explicit_file_oauth_store_mode() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.mcp_oauth_credentials_store_mode, @@ -1792,7 +1834,8 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( final_config.mcp_oauth_credentials_store_mode, OAuthCredentialsStoreMode::Keyring, @@ -2942,8 +2985,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()), @@ -2954,7 +2997,8 @@ fn cli_override_sets_compact_prompt() -> std::io::Result<()> { ConfigToml::default(), overrides, codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.compact_prompt.as_deref(), @@ -2964,8 +3008,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)?; @@ -2987,7 +3031,8 @@ fn loads_compact_prompt_from_file() -> std::io::Result<()> { cfg, overrides, codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.compact_prompt.as_deref(), @@ -2997,8 +3042,8 @@ fn loads_compact_prompt_from_file() -> std::io::Result<()> { Ok(()) } -#[test] -fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::Result<()> { let codex_home = TempDir::new()?; let config_layer_stack = ConfigLayerStack::new( Vec::new(), @@ -3020,7 +3065,8 @@ fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::R }, codex_home.path().to_path_buf(), config_layer_stack, - )?; + ) + .await?; assert_eq!( config.guardian_developer_instructions.as_deref(), @@ -3030,8 +3076,9 @@ fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::R Ok(()) } -#[test] -fn load_config_ignores_empty_requirements_guardian_developer_instructions() -> std::io::Result<()> { +#[tokio::test] +async fn load_config_ignores_empty_requirements_guardian_developer_instructions() +-> std::io::Result<()> { let codex_home = TempDir::new()?; let config_layer_stack = ConfigLayerStack::new( Vec::new(), @@ -3051,15 +3098,16 @@ fn load_config_ignores_empty_requirements_guardian_developer_instructions() -> s }, codex_home.path().to_path_buf(), config_layer_stack, - )?; + ) + .await?; assert_eq!(config.guardian_developer_instructions, 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 { @@ -3083,7 +3131,8 @@ fn load_config_rejects_missing_agent_role_config_file() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - ); + ) + .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(); @@ -3924,8 +3973,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 { @@ -3951,7 +4000,8 @@ fn load_config_normalizes_agent_role_nickname_candidates() -> std::io::Result<() cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config @@ -3965,8 +4015,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 { @@ -3989,7 +4039,8 @@ fn load_config_rejects_empty_agent_role_nickname_candidates() -> std::io::Result cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - ); + ) + .await; let err = result.expect_err("empty nickname candidates should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); assert!( @@ -4000,8 +4051,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 { @@ -4024,7 +4075,8 @@ fn load_config_rejects_duplicate_agent_role_nickname_candidates() -> std::io::Re cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - ); + ) + .await; let err = result.expect_err("duplicate nickname candidates should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); assert!( @@ -4035,8 +4087,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 { @@ -4059,7 +4111,8 @@ fn load_config_rejects_unsafe_agent_role_nickname_candidates() -> std::io::Resul cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - ); + ) + .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( @@ -4069,8 +4122,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: ModelsResponse = @@ -4090,14 +4143,15 @@ fn model_catalog_json_loads_from_path() -> std::io::Result<()> { cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .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":[]}"#)?; @@ -4112,6 +4166,7 @@ fn model_catalog_json_rejects_empty_catalog() -> std::io::Result<()> { ConfigOverrides::default(), codex_home.path().to_path_buf(), ) + .await .expect_err("empty custom catalog should fail config load"); assert_eq!(err.kind(), ErrorKind::InvalidData); @@ -4234,8 +4289,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 { @@ -4247,7 +4302,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()), @@ -4361,8 +4417,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( @@ -4372,14 +4428,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 { @@ -4391,7 +4448,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, @@ -4513,14 +4571,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 { @@ -4532,7 +4591,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, @@ -4646,8 +4706,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 { @@ -4659,7 +4719,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, @@ -4773,8 +4834,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 { @@ -4825,7 +4887,8 @@ fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> any }, fixture.codex_home(), config_layer_stack, - )?; + ) + .await?; assert!( !config @@ -4988,9 +5051,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()), @@ -5001,7 +5064,8 @@ fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error() -> cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - ); + ) + .await; assert!(result.is_err()); let error = result.unwrap_err(); assert_eq!(error.kind(), std::io::ErrorKind::NotFound); @@ -5014,8 +5078,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" @@ -5024,13 +5088,15 @@ trust_level = "untrusted" let cfg = toml::from_str::(config_with_untrusted) .expect("TOML deserialization should succeed"); - let resolution = cfg.derive_sandbox_policy( - None, - None, - WindowsSandboxLevel::Disabled, - &PathBuf::from("/tmp/test"), - None, - ); + let resolution = cfg + .derive_sandbox_policy( + None, + None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + None, + ) + .await; // Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade) if cfg!(target_os = "windows") { @@ -5048,9 +5114,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(); @@ -5076,21 +5142,23 @@ fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults() } })?; - let resolution = cfg.derive_sandbox_policy( - None, - None, - WindowsSandboxLevel::Disabled, - &project_path, - Some(&constrained), - ); + let resolution = cfg + .derive_sandbox_policy( + None, + 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(); @@ -5116,13 +5184,15 @@ fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallback() } })?; - let resolution = cfg.derive_sandbox_policy( - None, - None, - WindowsSandboxLevel::Disabled, - &project_path, - Some(&constrained), - ); + let resolution = cfg + .derive_sandbox_policy( + None, + None, + WindowsSandboxLevel::Disabled, + &project_path, + Some(&constrained), + ) + .await; if cfg!(target_os = "windows") { assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); @@ -5230,8 +5300,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" @@ -5244,14 +5314,15 @@ mcp_oauth_callback_port = 5678 cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .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#" @@ -5265,14 +5336,15 @@ allow_login_shell = false cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .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" @@ -5285,7 +5357,8 @@ mcp_oauth_callback_url = "https://example.com/callback" cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.mcp_oauth_callback_url.as_deref(), @@ -5294,8 +5367,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(); @@ -5315,7 +5388,8 @@ fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow::Resul ..Default::default() }, codex_home.path().to_path_buf(), - )?; + ) + .await?; // Verify that untrusted projects get UnlessTrusted approval policy assert_eq!( @@ -5855,8 +5929,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] @@ -5894,7 +5968,8 @@ discoverables = [ cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.tool_suggest, @@ -5914,8 +5989,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" @@ -5933,7 +6008,8 @@ experimental_realtime_start_instructions = "start instructions from config" cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_start_instructions.as_deref(), @@ -5942,8 +6018,8 @@ experimental_realtime_start_instructions = "start instructions from config" Ok(()) } -#[test] -fn experimental_exec_server_url_loads_from_config_toml() -> std::io::Result<()> { +#[tokio::test] +async fn experimental_exec_server_url_loads_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" experimental_exec_server_url = "http://127.0.0.1:8080" @@ -5961,7 +6037,8 @@ experimental_exec_server_url = "http://127.0.0.1:8080" cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.experimental_exec_server_url.as_deref(), @@ -5970,8 +6047,8 @@ experimental_exec_server_url = "http://127.0.0.1:8080" 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" @@ -5989,7 +6066,8 @@ experimental_realtime_ws_base_url = "http://127.0.0.1:8011" cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_ws_base_url.as_deref(), @@ -5998,8 +6076,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" @@ -6017,7 +6095,8 @@ experimental_realtime_ws_backend_prompt = "prompt from config" cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_ws_backend_prompt.as_deref(), @@ -6026,8 +6105,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" @@ -6045,7 +6124,8 @@ experimental_realtime_ws_startup_context = "startup context from config" cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_ws_startup_context.as_deref(), @@ -6054,8 +6134,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" @@ -6073,7 +6153,8 @@ experimental_realtime_ws_model = "realtime-test-model" cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.experimental_realtime_ws_model.as_deref(), @@ -6082,8 +6163,8 @@ experimental_realtime_ws_model = "realtime-test-model" 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] @@ -6106,7 +6187,8 @@ type = "transcription" cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .await?; assert_eq!( config.realtime, @@ -6118,8 +6200,8 @@ type = "transcription" 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] @@ -6141,7 +6223,8 @@ speaker = "Desk Speakers" cfg, ConfigOverrides::default(), codex_home.path().to_path_buf(), - )?; + ) + .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 dc04d9bb14..17c45e26a1 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -209,13 +209,14 @@ fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option { } } #[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(), codex_home.path().to_path_buf(), ) + .await .expect("load default test config") } @@ -722,6 +723,7 @@ impl ConfigBuilder { codex_home, config_layer_stack, ) + .await } } @@ -737,7 +739,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()?; @@ -756,6 +758,7 @@ impl Config { codex_home, ConfigLayerStack::default(), ) + .await } /// This is a secondary way of creating [Config], which is appropriate when @@ -1660,7 +1663,7 @@ pub struct GhostSnapshotToml { impl ConfigToml { /// Derive the effective sandbox policy from the configuration. - fn derive_sandbox_policy( + async fn derive_sandbox_policy( &self, sandbox_mode_override: Option, profile_sandbox_mode: Option, @@ -1671,15 +1674,13 @@ impl ConfigToml { let sandbox_mode_was_explicit = sandbox_mode_override.is_some() || profile_sandbox_mode.is_some() || self.sandbox_mode.is_some(); - let resolved_sandbox_mode = sandbox_mode_override - .or(profile_sandbox_mode) - .or(self.sandbox_mode) - .or_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| { - if p.is_trusted() || p.is_untrusted() { + let trust_default_sandbox_mode = if sandbox_mode_was_explicit { + None + } else { + self.get_active_project(resolved_cwd) + .await + .and_then(|project| { + if project.is_trusted() || project.is_untrusted() { if cfg!(target_os = "windows") && windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Disabled @@ -1692,7 +1693,11 @@ impl ConfigToml { None } }) - }) + }; + let resolved_sandbox_mode = sandbox_mode_override + .or(profile_sandbox_mode) + .or(self.sandbox_mode) + .or(trust_default_sandbox_mode) .unwrap_or_default(); let mut sandbox_policy = match resolved_sandbox_mode { SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(), @@ -1742,7 +1747,7 @@ 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 projects = self.projects.clone().unwrap_or_default(); if let Some(project_config) = projects.get(&resolved_cwd.to_string_lossy().to_string()) { @@ -1752,7 +1757,7 @@ 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) + if let Some(repo_root) = resolve_root_git_project_for_trust(resolved_cwd).await && let Some(project_config_for_root) = projects.get(&repo_root.to_string_lossy().to_string_lossy().to_string()) { @@ -1999,17 +2004,17 @@ 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: PathBuf, ) -> 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: PathBuf, @@ -2128,6 +2133,7 @@ impl Config { .collect::, _>>()?; let active_project = cfg .get_active_project(&resolved_cwd) + .await .unwrap_or(ProjectConfig { trust_level: None }); let permission_config_syntax = resolve_permission_config_syntax( &config_layer_stack, @@ -2215,13 +2221,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, - Some(&constrained_sandbox_policy), - ); + let mut sandbox_policy = cfg + .derive_sandbox_policy( + sandbox_mode, + config_profile.sandbox_mode, + windows_sandbox_level, + &resolved_cwd, + 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) { diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index b90b903e88..bf1ab5f9c5 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -14,8 +14,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"); @@ -54,7 +54,8 @@ fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> ..Default::default() }, 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 0c426b155f..f57457b669 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -686,7 +686,7 @@ async fn project_trust_context( let projects = project_trust_config.projects.unwrap_or_default(); let project_root_key = project_root.as_path().to_string_lossy().to_string(); - 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| root.to_string_lossy().to_string()); diff --git a/codex-rs/core/src/git_info.rs b/codex-rs/core/src/git_info.rs index 052f786bfa..e115ceabd1 100644 --- a/codex-rs/core/src/git_info.rs +++ b/codex-rs/core/src/git_info.rs @@ -1,12 +1,16 @@ use std::collections::BTreeMap; use std::collections::HashSet; use std::ffi::OsStr; +use std::io; use std::path::Path; use std::path::PathBuf; use crate::util::resolve_path; use codex_app_server_protocol::GitSha; +use codex_exec_server::ExecutorFileSystem; +use codex_exec_server::LOCAL_FS; use codex_protocol::protocol::GitInfo; +use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::join_all; use serde::Deserialize; use serde::Serialize; @@ -603,20 +607,32 @@ 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 { - 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() { - return Some(canonicalize_or_raw(repo_root)); +pub async fn resolve_root_git_project_for_trust(cwd: &Path) -> Option { + resolve_root_git_project_for_trust_with_fs(&LOCAL_FS, cwd).await +} + +pub async fn resolve_root_git_project_for_trust_with_fs( + file_system: &dyn ExecutorFileSystem, + cwd: &Path, +) -> Option { + let base = trust_lookup_base_dir(file_system, cwd).await?; + let (repo_root, dot_git, dot_git_kind) = + find_ancestor_git_entry_with_fs(file_system, &base).await?; + if dot_git_kind == EntryKind::Directory { + return Some(normalize_absolute_or_raw(&repo_root)); } - let git_dir_s = std::fs::read_to_string(&dot_git).ok()?; + let git_dir_s = file_system + .read_to_string(&AbsolutePathBuf::try_from(dot_git.as_path()).ok()?) + .await + .ok()?; let git_dir_rel = git_dir_s.trim().strip_prefix("gitdir:")?.trim(); if git_dir_rel.is_empty() { return None; } - let git_dir_path = canonicalize_or_raw(resolve_path(&repo_root, &PathBuf::from(git_dir_rel))); + let git_dir_path = + normalize_absolute_or_raw(&resolve_path(&repo_root, &PathBuf::from(git_dir_rel))); let worktrees_dir = git_dir_path.parent()?; if worktrees_dir.file_name() != Some(OsStr::new("worktrees")) { return None; @@ -624,7 +640,66 @@ pub fn resolve_root_git_project_for_trust(cwd: &Path) -> Option { let common_dir = worktrees_dir.parent()?; let main_repo_root = common_dir.parent()?; - Some(canonicalize_or_raw(main_repo_root.to_path_buf())) + Some(normalize_absolute_or_raw(main_repo_root)) +} + +async fn trust_lookup_base_dir( + file_system: &dyn ExecutorFileSystem, + cwd: &Path, +) -> Option { + if entry_kind(file_system, cwd).await.ok() == Some(EntryKind::Directory) { + Some(cwd.to_path_buf()) + } else { + cwd.parent().map(Path::to_path_buf) + } +} + +async fn find_ancestor_git_entry_with_fs( + file_system: &dyn ExecutorFileSystem, + base_dir: &Path, +) -> Option<(PathBuf, PathBuf, EntryKind)> { + let mut dir = base_dir.to_path_buf(); + + loop { + let dot_git = dir.join(".git"); + if let Ok(dot_git_kind) = entry_kind(file_system, &dot_git).await { + return Some((dir, dot_git, dot_git_kind)); + } + + if !dir.pop() { + break; + } + } + + None +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum EntryKind { + Directory, + File, +} + +async fn entry_kind(file_system: &dyn ExecutorFileSystem, path: &Path) -> io::Result { + let metadata = file_system + .get_metadata(&AbsolutePathBuf::try_from(path).map_err(io::Error::other)?) + .await?; + if metadata.is_directory { + Ok(EntryKind::Directory) + } else if metadata.is_file { + Ok(EntryKind::File) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("unsupported filesystem entry type: {}", path.display()), + )) + } +} + +fn normalize_absolute_or_raw(path: &Path) -> PathBuf { + AbsolutePathBuf::try_from(path) + .map(AbsolutePathBuf::into_path_buf) + .unwrap_or_else(|_| path.to_path_buf()) } fn find_ancestor_git_entry(base_dir: &Path) -> Option<(PathBuf, PathBuf)> { @@ -646,10 +721,6 @@ fn find_ancestor_git_entry(base_dir: &Path) -> Option<(PathBuf, PathBuf)> { None } -fn canonicalize_or_raw(path: PathBuf) -> PathBuf { - std::fs::canonicalize(&path).unwrap_or(path) -} - /// Returns a list of local git branches. /// Includes the default branch at the beginning of the list, if it exists. pub async fn local_git_branches(cwd: &Path) -> Vec { diff --git a/codex-rs/core/src/git_info_tests.rs b/codex-rs/core/src/git_info_tests.rs index 73714ce42f..c49d81c9a1 100644 --- a/codex-rs/core/src/git_info_tests.rs +++ b/codex-rs/core/src/git_info_tests.rs @@ -1,5 +1,6 @@ use super::*; +use codex_exec_server::LOCAL_FS; use core_test_support::skip_if_sandbox; use std::fs; use std::path::PathBuf; @@ -420,10 +421,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] @@ -433,12 +438,32 @@ 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] +async fn resolve_root_git_project_for_trust_with_fs_regular_repo_returns_repo_root() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let repo_path = create_test_git_repo(&temp_dir).await; + let expected = std::fs::canonicalize(&repo_path).unwrap(); + assert_eq!( + resolve_root_git_project_for_trust_with_fs(&LOCAL_FS, &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_with_fs(&LOCAL_FS, &nested).await, + Some(expected) + ); } #[tokio::test] @@ -461,18 +486,51 @@ 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(|path| std::fs::canonicalize(path).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(|path| std::fs::canonicalize(path).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_with_fs_detects_worktree_and_returns_main_root() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let repo_path = create_test_git_repo(&temp_dir).await; + + let wt_root = temp_dir.path().join("wt"); + let _ = std::process::Command::new("git") + .args([ + "worktree", + "add", + wt_root.to_str().unwrap(), + "-b", + "feature/x", + ]) + .current_dir(&repo_path) + .output() + .expect("git worktree add"); + + let expected = std::fs::canonicalize(&repo_path).ok(); + let got = resolve_root_git_project_for_trust_with_fs(&LOCAL_FS, &wt_root) + .await + .and_then(|path| std::fs::canonicalize(path).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_with_fs(&LOCAL_FS, &nested) + .await + .and_then(|path| std::fs::canonicalize(path).ok()); + assert_eq!(got_nested, expected); +} + +#[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"); @@ -489,17 +547,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(); @@ -514,8 +572,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 50bf2ed843..3f9ca631b3 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -743,9 +743,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, None, "active-model", None) .expect("cached guardian config"); diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 89c22528e8..6bd84506c8 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -963,9 +963,9 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a Ok(()) } -#[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 { @@ -1005,9 +1005,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()); @@ -1021,9 +1021,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.network.allowed_domains = vec!["parent.example".to_string()]; @@ -1061,9 +1061,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 { @@ -1085,9 +1085,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 = @@ -1097,8 +1097,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_override() { +#[tokio::test] +async fn guardian_review_session_config_uses_requirements_guardian_override() { let codex_home = tempfile::tempdir().expect("create temp dir"); let workspace = tempfile::tempdir().expect("create temp dir"); let config_layer_stack = ConfigLayerStack::new( @@ -1121,6 +1121,7 @@ fn guardian_review_session_config_uses_requirements_guardian_override() { codex_home.path().to_path_buf(), config_layer_stack, ) + .await .expect("load config"); let guardian_config = @@ -1133,8 +1134,9 @@ fn guardian_review_session_config_uses_requirements_guardian_override() { ); } -#[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 = @@ -1149,6 +1151,7 @@ fn guardian_review_session_config_uses_default_guardian_policy_without_requireme codex_home.path().to_path_buf(), config_layer_stack, ) + .await .expect("load config"); let guardian_config = diff --git a/codex-rs/core/src/mcp/mod_tests.rs b/codex-rs/core/src/mcp/mod_tests.rs index dc9465e103..b6f7f542a0 100644 --- a/codex-rs/core/src/mcp/mod_tests.rs +++ b/codex-rs/core/src/mcp/mod_tests.rs @@ -143,9 +143,9 @@ fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() { ); } -#[test] -fn codex_apps_mcp_url_uses_legacy_codex_apps_path() { - let mut config = crate::config::test_config(); +#[tokio::test] +async fn codex_apps_mcp_url_uses_legacy_codex_apps_path() { + let mut config = crate::config::test_config().await; config.chatgpt_base_url = "https://chatgpt.com".to_string(); assert_eq!( @@ -154,9 +154,9 @@ fn codex_apps_mcp_url_uses_legacy_codex_apps_path() { ); } -#[test] -fn codex_apps_server_config_uses_legacy_codex_apps_path() { - let mut config = crate::config::test_config(); +#[tokio::test] +async fn codex_apps_server_config_uses_legacy_codex_apps_path() { + let mut config = crate::config::test_config().await; config.chatgpt_base_url = "https://chatgpt.com".to_string(); let mut servers = with_codex_apps_mcp(HashMap::new(), false, None, &config); diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index e04a018a84..fa6d02cbc8 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -467,7 +467,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_home.path().to_path_buf(); config.cwd = config.codex_home.clone(); let config = Arc::new(config); @@ -880,7 +880,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_home.path().to_path_buf(); config.cwd = config.codex_home.clone(); let config = Arc::new(config); diff --git a/codex-rs/core/src/models_manager/model_info_tests.rs b/codex-rs/core/src/models_manager/model_info_tests.rs index 27bac8d70b..2963407bcf 100644 --- a/codex-rs/core/src/models_manager/model_info_tests.rs +++ b/codex-rs/core/src/models_manager/model_info_tests.rs @@ -2,10 +2,10 @@ use super::*; use crate::config::test_config; use pretty_assertions::assert_eq; -#[test] -fn reasoning_summaries_override_true_enables_support() { +#[tokio::test] +async fn reasoning_summaries_override_true_enables_support() { let model = model_info_from_slug("unknown-model"); - let mut config = test_config(); + let mut config = test_config().await; config.model_supports_reasoning_summaries = Some(true); let updated = with_config_overrides(model.clone(), &config); @@ -15,11 +15,11 @@ fn reasoning_summaries_override_true_enables_support() { assert_eq!(updated, expected); } -#[test] -fn reasoning_summaries_override_false_does_not_disable_support() { +#[tokio::test] +async fn reasoning_summaries_override_false_does_not_disable_support() { let mut model = model_info_from_slug("unknown-model"); model.supports_reasoning_summaries = true; - let mut config = test_config(); + let mut config = test_config().await; config.model_supports_reasoning_summaries = Some(false); let updated = with_config_overrides(model.clone(), &config); @@ -27,10 +27,10 @@ fn reasoning_summaries_override_false_does_not_disable_support() { assert_eq!(updated, model); } -#[test] -fn reasoning_summaries_override_false_is_noop_when_model_is_false() { +#[tokio::test] +async fn reasoning_summaries_override_false_is_noop_when_model_is_false() { let model = model_info_from_slug("unknown-model"); - let mut config = test_config(); + let mut config = test_config().await; config.model_supports_reasoning_summaries = Some(false); let updated = with_config_overrides(model.clone(), &config); diff --git a/codex-rs/core/src/original_image_detail_tests.rs b/codex-rs/core/src/original_image_detail_tests.rs index e4a3c09880..2a32952eaa 100644 --- a/codex-rs/core/src/original_image_detail_tests.rs +++ b/codex-rs/core/src/original_image_detail_tests.rs @@ -5,9 +5,9 @@ use crate::models_manager::manager::ModelsManager; use codex_features::Features; use pretty_assertions::assert_eq; -#[test] -fn image_detail_original_feature_enables_explicit_original_without_force() { - let config = test_config(); +#[tokio::test] +async fn image_detail_original_feature_enables_explicit_original_without_force() { + let config = test_config().await; let mut model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); model_info.supports_image_detail_original = true; @@ -25,9 +25,9 @@ fn image_detail_original_feature_enables_explicit_original_without_force() { ); } -#[test] -fn explicit_original_is_dropped_without_feature_or_model_support() { - let config = test_config(); +#[tokio::test] +async fn explicit_original_is_dropped_without_feature_or_model_support() { + let config = test_config().await; let mut model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); model_info.supports_image_detail_original = true; @@ -47,9 +47,9 @@ fn explicit_original_is_dropped_without_feature_or_model_support() { ); } -#[test] -fn unsupported_non_original_detail_is_dropped() { - let config = test_config(); +#[tokio::test] +async fn unsupported_non_original_detail_is_dropped() { + let config = test_config().await; let mut model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); model_info.supports_image_detail_original = true; diff --git a/codex-rs/core/src/realtime_context.rs b/codex-rs/core/src/realtime_context.rs index 5e714a8308..280f8a9d6d 100644 --- a/codex-rs/core/src/realtime_context.rs +++ b/codex-rs/core/src/realtime_context.rs @@ -2,9 +2,11 @@ use crate::codex::Session; use crate::compact::content_items_to_text; use crate::event_mapping::is_contextual_user_message_content; use crate::git_info::resolve_root_git_project_for_trust; +use crate::git_info::resolve_root_git_project_for_trust_with_fs; use crate::truncate::TruncationPolicy; use crate::truncate::truncate_text; use chrono::Utc; +use codex_exec_server::ExecutorFileSystem; use codex_protocol::models::ResponseItem; use codex_state::SortKey; use codex_state::ThreadMetadata; @@ -54,11 +56,13 @@ pub(crate) async fn build_realtime_startup_context( ) -> Option { let config = sess.get_config().await; let cwd = config.cwd.clone(); + let file_system = sess.services.environment.get_filesystem(); 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(file_system.as_ref(), &cwd, home_dir()).await; if current_thread_section.is_none() && recent_work_section.is_none() @@ -141,46 +145,57 @@ async fn load_recent_threads(sess: &Session) -> Vec { } } -fn build_recent_work_section(cwd: &Path, recent_threads: &[ThreadMetadata]) -> Option { - let mut groups: HashMap> = HashMap::new(); +async fn build_recent_work_section( + cwd: &Path, + recent_threads: &[ThreadMetadata], +) -> 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()); - groups.entry(group).or_default().push(entry); + let git_root = resolve_root_git_project_for_trust(&entry.cwd).await; + let is_git_repo = git_root.is_some(); + let group = git_root.unwrap_or_else(|| entry.cwd.clone()); + let (group_is_git_repo, entries) = groups + .entry(group) + .or_insert_with(|| (is_git_repo, Vec::new())); + *group_is_git_repo |= is_git_repo; + entries.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 - .iter() - .map(|entry| entry.updated_at) - .max() - .unwrap_or_else(Utc::now); - let right_latest = right_entries - .iter() - .map(|entry| entry.updated_at) - .max() - .unwrap_or_else(Utc::now); - ( - *left_group != current_group, - Reverse(left_latest), - left_group.as_os_str(), - ) - .cmp(&( - *right_group != current_group, - Reverse(right_latest), - right_group.as_os_str(), - )) - }); + groups.sort_by( + |(left_group, (_, left_entries)), (right_group, (_, right_entries))| { + let left_latest = left_entries + .iter() + .map(|entry| entry.updated_at) + .max() + .unwrap_or_else(Utc::now); + let right_latest = right_entries + .iter() + .map(|entry| entry.updated_at) + .max() + .unwrap_or_else(Utc::now); + ( + *left_group != current_group, + Reverse(left_latest), + left_group.as_os_str(), + ) + .cmp(&( + *right_group != current_group, + Reverse(right_latest), + right_group.as_os_str(), + )) + }, + ); let sections = groups .into_iter() .take(MAX_RECENT_WORK_GROUPS) - .filter_map(|(group, mut entries)| { + .filter_map(|(group, (is_git_repo, mut entries))| { entries.sort_by_key(|entry| Reverse(entry.updated_at)); - format_thread_group(¤t_group, &group, entries) + format_thread_group(¤t_group, &group, is_git_repo, entries) }) .collect::>(); (!sections.is_empty()).then(|| sections.join("\n\n")) @@ -270,11 +285,12 @@ fn build_current_thread_section(items: &[ResponseItem]) -> Option { Some(lines.join("\n")) } -fn build_workspace_section_with_user_root( +async fn build_workspace_section_with_user_root( + file_system: &dyn ExecutorFileSystem, 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_with_fs(file_system, cwd).await; let cwd_tree = render_tree(cwd); let git_root_tree = git_root .as_ref() @@ -412,10 +428,11 @@ fn format_section(title: &str, body: Option, budget_tokens: usize) -> Op fn format_thread_group( current_group: &Path, group: &Path, + is_git_repo: bool, entries: Vec<&ThreadMetadata>, ) -> 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 is_git_repo { 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 a19abf3edd..5c61105722 100644 --- a/codex-rs/core/src/realtime_context_tests.rs +++ b/codex-rs/core/src/realtime_context_tests.rs @@ -2,6 +2,7 @@ use super::build_recent_work_section; use super::build_workspace_section_with_user_root; use chrono::TimeZone; use chrono::Utc; +use codex_exec_server::LOCAL_FS; use codex_protocol::ThreadId; use codex_state::ThreadMetadata; use pretty_assertions::assert_eq; @@ -43,30 +44,31 @@ fn thread_metadata(cwd: &str, title: &str, first_user_message: &str) -> ThreadMe } } -#[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(), None), + build_workspace_section_with_user_root(&LOCAL_FS, cwd.path(), 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(), None).expect("workspace section"); + let section = build_workspace_section_with_user_root(&LOCAL_FS, cwd.path(), 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"); @@ -79,15 +81,16 @@ fn workspace_section_includes_user_root_tree_when_distinct() { fs::create_dir_all(user_root.join("code")).expect("create user root child"); 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)) + let section = build_workspace_section_with_user_root(&LOCAL_FS, 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"); @@ -123,6 +126,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/rollout/metadata_tests.rs b/codex-rs/core/src/rollout/metadata_tests.rs index dacd9e67b8..8a3063b389 100644 --- a/codex-rs/core/src/rollout/metadata_tests.rs +++ b/codex-rs/core/src/rollout/metadata_tests.rs @@ -197,7 +197,7 @@ async fn backfill_sessions_resumes_from_watermark_and_marks_complete() { )) .await; - let mut config = crate::config::test_config(); + let mut config = crate::config::test_config().await; config.codex_home = codex_home.clone(); config.model_provider_id = "test-provider".to_string(); backfill_sessions(runtime.as_ref(), &config).await; @@ -267,7 +267,7 @@ async fn backfill_sessions_preserves_existing_git_branch_and_fills_missing_git_f .await .expect("existing metadata upsert"); - let mut config = crate::config::test_config(); + let mut config = crate::config::test_config().await; config.codex_home = codex_home.clone(); config.model_provider_id = "test-provider".to_string(); backfill_sessions(runtime.as_ref(), &config).await; @@ -304,7 +304,7 @@ async fn backfill_sessions_normalizes_cwd_before_upsert() { .await .expect("initialize runtime"); - let mut config = crate::config::test_config(); + let mut config = crate::config::test_config().await; config.codex_home = codex_home.clone(); config.model_provider_id = "test-provider".to_string(); backfill_sessions(runtime.as_ref(), &config).await; diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 40e7439ccc..ba74006f05 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -234,7 +234,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"); config.cwd = config.codex_home.clone(); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); @@ -273,7 +273,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"); config.cwd = config.codex_home.clone(); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); @@ -406,7 +406,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"); config.cwd = config.codex_home.clone(); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); @@ -503,7 +503,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"); config.cwd = config.codex_home.clone(); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); @@ -589,7 +589,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"); config.cwd = config.codex_home.clone(); 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 9daaab9c6b..fbd3c1ae80 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -54,8 +54,8 @@ fn windows_shell_safety_description() -> String { format!("\n\n{}", super::windows_destructive_filesystem_guidance()) } -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 = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); model_info.supports_search_tool = true; @@ -358,8 +358,8 @@ fn strip_descriptions_tool(spec: &mut ToolSpec) { } } -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: ModelsResponse = serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); let model = response @@ -394,9 +394,9 @@ fn unified_exec_is_blocked_for_windows_sandboxed_policies_only() { )); } -#[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(); @@ -418,9 +418,9 @@ fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() { assert_eq!(config.shell_type, expected_shell_type); } -#[test] -fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { - let model_info = model_info_from_models_json("gpt-5-codex"); +#[tokio::test] +async fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { + let model_info = model_info_from_models_json("gpt-5-codex").await; let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -503,9 +503,9 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { } } -#[test] -fn test_build_specs_collab_tools_enabled() { - let config = test_config(); +#[tokio::test] +async fn test_build_specs_collab_tools_enabled() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); @@ -528,9 +528,9 @@ fn test_build_specs_collab_tools_enabled() { assert_lacks_tool_name(&tools, "list_agents"); } -#[test] -fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { - let config = test_config(); +#[tokio::test] +async fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); @@ -655,9 +655,9 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { assert_lacks_tool_name(&tools, "resume_agent"); } -#[test] -fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { - let config = test_config(); +#[tokio::test] +async fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::SpawnCsv); @@ -685,9 +685,9 @@ fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { ); } -#[test] -fn view_image_tool_omits_detail_without_original_detail_feature() { - let config = test_config(); +#[tokio::test] +async fn view_image_tool_omits_detail_without_original_detail_feature() { + let config = test_config().await; let mut model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); model_info.supports_image_detail_original = true; @@ -713,9 +713,9 @@ fn view_image_tool_omits_detail_without_original_detail_feature() { assert!(!properties.contains_key("detail")); } -#[test] -fn view_image_tool_includes_detail_with_original_detail_feature() { - let config = test_config(); +#[tokio::test] +async fn view_image_tool_includes_detail_with_original_detail_feature() { + let config = test_config().await; let mut model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); model_info.supports_image_detail_original = true; @@ -750,9 +750,9 @@ fn view_image_tool_includes_detail_with_original_detail_feature() { assert!(description.contains("omit this field for default resized behavior")); } -#[test] -fn test_build_specs_artifact_tool_enabled() { - let mut config = test_config(); +#[tokio::test] +async fn test_build_specs_artifact_tool_enabled() { + let mut config = test_config().await; let runtime_root = tempfile::TempDir::new().expect("create temp codex home"); config.codex_home = runtime_root.path().to_path_buf(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); @@ -772,9 +772,9 @@ fn test_build_specs_artifact_tool_enabled() { assert_contains_tool_names(&tools, &["artifacts"]); } -#[test] -fn test_build_specs_agent_job_worker_tools_enabled() { - let config = test_config(); +#[tokio::test] +async fn test_build_specs_agent_job_worker_tools_enabled() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::SpawnCsv); @@ -808,9 +808,9 @@ fn test_build_specs_agent_job_worker_tools_enabled() { assert_lacks_tool_name(&tools, "request_user_input"); } -#[test] -fn request_user_input_description_reflects_default_mode_feature_flag() { - let config = test_config(); +#[tokio::test] +async fn request_user_input_description_reflects_default_mode_feature_flag() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); let available_models = Vec::new(); @@ -851,9 +851,9 @@ fn request_user_input_description_reflects_default_mode_feature_flag() { ); } -#[test] -fn request_permissions_requires_feature_flag() { - let config = test_config(); +#[tokio::test] +async fn request_permissions_requires_feature_flag() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); let available_models = Vec::new(); @@ -889,9 +889,9 @@ fn request_permissions_requires_feature_flag() { ); } -#[test] -fn request_permissions_tool_is_independent_from_additional_permissions() { - let config = test_config(); +#[tokio::test] +async fn request_permissions_tool_is_independent_from_additional_permissions() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::ExecPermissionApprovals); @@ -910,9 +910,9 @@ fn request_permissions_tool_is_independent_from_additional_permissions() { assert_lacks_tool_name(&tools, "request_permissions"); } -#[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 = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.disable(Feature::MemoryTool); @@ -933,9 +933,9 @@ fn get_memory_requires_feature_flag() { ); } -#[test] -fn js_repl_requires_feature_flag() { - let config = test_config(); +#[tokio::test] +async fn js_repl_requires_feature_flag() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); @@ -961,9 +961,9 @@ fn js_repl_requires_feature_flag() { ); } -#[test] -fn js_repl_enabled_adds_tools() { - let config = test_config(); +#[tokio::test] +async fn js_repl_enabled_adds_tools() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::JsRepl); @@ -982,9 +982,9 @@ fn js_repl_enabled_adds_tools() { assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]); } -#[test] -fn image_generation_tools_require_feature_and_supported_model() { - let config = test_config(); +#[tokio::test] +async fn image_generation_tools_require_feature_and_supported_model() { + let config = test_config().await; let mut supported_model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5.2", &config); supported_model_info.slug = "custom/gpt-5.2-variant".to_string(); @@ -1065,14 +1065,14 @@ fn js_repl_freeform_grammar_blocks_common_non_js_prefixes() { assert!(!format.definition.contains("(?!")); } -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, @@ -1100,7 +1100,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, @@ -1113,12 +1113,12 @@ 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 web_search_mode_cached_sets_external_web_access_false() { - let config = test_config(); +#[tokio::test] +async fn web_search_mode_cached_sets_external_web_access_false() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); @@ -1147,9 +1147,9 @@ fn web_search_mode_cached_sets_external_web_access_false() { ); } -#[test] -fn web_search_mode_live_sets_external_web_access_true() { - let config = test_config(); +#[tokio::test] +async fn web_search_mode_live_sets_external_web_access_true() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); @@ -1178,9 +1178,9 @@ fn web_search_mode_live_sets_external_web_access_true() { ); } -#[test] -fn web_search_config_is_forwarded_to_tool_spec() { - let config = test_config(); +#[tokio::test] +async fn web_search_config_is_forwarded_to_tool_spec() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); let web_search_config = WebSearchConfig { @@ -1227,9 +1227,9 @@ fn web_search_config_is_forwarded_to_tool_spec() { ); } -#[test] -fn web_search_tool_type_text_and_image_sets_search_content_types() { - let config = test_config(); +#[tokio::test] +async fn web_search_tool_type_text_and_image_sets_search_content_types() { + let config = test_config().await; let mut model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); model_info.web_search_tool_type = WebSearchToolType::TextAndImage; @@ -1265,9 +1265,9 @@ fn web_search_tool_type_text_and_image_sets_search_content_types() { ); } -#[test] -fn mcp_resource_tools_are_hidden_without_mcp_servers() { - let config = test_config(); +#[tokio::test] +async fn mcp_resource_tools_are_hidden_without_mcp_servers() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); let available_models = Vec::new(); @@ -1291,9 +1291,9 @@ fn mcp_resource_tools_are_hidden_without_mcp_servers() { ); } -#[test] -fn mcp_resource_tools_are_included_when_mcp_servers_are_present() { - let config = test_config(); +#[tokio::test] +async fn mcp_resource_tools_are_included_when_mcp_servers_are_present() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let features = Features::with_defaults(); let available_models = Vec::new(); @@ -1318,8 +1318,8 @@ fn mcp_resource_tools_are_included_when_mcp_servers_are_present() { ); } -#[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", @@ -1338,11 +1338,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", @@ -1361,11 +1362,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( @@ -1386,11 +1388,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( @@ -1411,11 +1414,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", @@ -1434,11 +1438,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", @@ -1457,11 +1462,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", @@ -1479,11 +1485,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", @@ -1502,11 +1509,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( @@ -1527,12 +1535,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 = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1556,9 +1565,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 = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1619,10 +1628,10 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { ); } -#[test] +#[tokio::test] #[ignore] -fn test_parallel_support_flags() { - let config = test_config(); +async fn test_parallel_support_flags() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1645,10 +1654,10 @@ fn test_parallel_support_flags() { assert!(find_tool(&tools, "read_file").supports_parallel_tool_calls); } -#[test] -fn test_test_model_info_includes_sync_tool() { - let _config = test_config(); - let mut model_info = model_info_from_models_json("gpt-5-codex"); +#[tokio::test] +async fn test_test_model_info_includes_sync_tool() { + let _config = test_config().await; + let mut model_info = model_info_from_models_json("gpt-5-codex").await; model_info.experimental_supported_tools = vec![ "test_sync_tool".to_string(), "read_file".to_string(), @@ -1686,9 +1695,9 @@ fn test_test_model_info_includes_sync_tool() { assert!(tools.iter().any(|tool| tool_name(&tool.spec) == "list_dir")); } -#[test] -fn test_build_specs_mcp_tools_converted() { - let config = test_config(); +#[tokio::test] +async fn test_build_specs_mcp_tools_converted() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1779,9 +1788,9 @@ fn test_build_specs_mcp_tools_converted() { ); } -#[test] -fn test_build_specs_mcp_tools_sorted_by_name() { - let config = test_config(); +#[tokio::test] +async fn test_build_specs_mcp_tools_sorted_by_name() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -1828,9 +1837,9 @@ fn test_build_specs_mcp_tools_sorted_by_name() { assert_eq!(mcp_names, expected); } -#[test] -fn search_tool_description_lists_each_codex_apps_connector_once() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn search_tool_description_lists_each_codex_apps_connector_once() { + let model_info = search_capable_model_info().await; let mut features = Features::with_defaults(); features.enable(Feature::Apps); let available_models = Vec::new(); @@ -1950,9 +1959,9 @@ fn search_tool_description_lists_each_codex_apps_connector_once() { assert!(!description.contains("mcp__rmcp__echo")); } -#[test] -fn search_tool_requires_model_capability_only() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn search_tool_requires_model_capability_only() { + let model_info = search_capable_model_info().await; let app_tools = Some(HashMap::from([( "mcp__codex_apps__calendar_create_event".to_string(), ToolInfo { @@ -2001,9 +2010,9 @@ fn search_tool_requires_model_capability_only() { assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]); } -#[test] -fn tool_suggest_is_not_registered_without_feature_flag() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn tool_suggest_is_not_registered_without_feature_flag() { + let model_info = search_capable_model_info().await; let mut features = Features::with_defaults(); features.enable(Feature::Apps); let available_models = Vec::new(); @@ -2036,9 +2045,9 @@ fn tool_suggest_is_not_registered_without_feature_flag() { ); } -#[test] -fn search_tool_description_handles_no_enabled_apps() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn search_tool_description_handles_no_enabled_apps() { + let model_info = search_capable_model_info().await; let mut features = Features::with_defaults(); features.enable(Feature::Apps); let available_models = Vec::new(); @@ -2062,9 +2071,9 @@ fn search_tool_description_handles_no_enabled_apps() { assert!(!description.contains("{{app_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); let available_models = Vec::new(); @@ -2110,9 +2119,9 @@ fn search_tool_description_falls_back_to_connector_name_without_description() { assert!(!description.contains("- Calendar:")); } -#[test] -fn search_tool_registers_namespaced_app_tool_aliases() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn search_tool_registers_namespaced_app_tool_aliases() { + let model_info = search_capable_model_info().await; let mut features = Features::with_defaults(); features.enable(Feature::Apps); let available_models = Vec::new(); @@ -2175,9 +2184,9 @@ fn search_tool_registers_namespaced_app_tool_aliases() { assert!(registry.has_handler(alias.as_str(), None)); } -#[test] -fn tool_suggest_description_lists_discoverable_tools() { - let model_info = search_capable_model_info(); +#[tokio::test] +async fn tool_suggest_description_lists_discoverable_tools() { + let model_info = search_capable_model_info().await; let mut features = Features::with_defaults(); features.enable(Feature::Apps); features.enable(Feature::ToolSuggest); @@ -2256,9 +2265,9 @@ fn tool_suggest_description_lists_discoverable_tools() { ); } -#[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 = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -2316,9 +2325,9 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() { ); } -#[test] -fn test_mcp_tool_integer_normalized_to_number() { - let config = test_config(); +#[tokio::test] +async fn test_mcp_tool_integer_normalized_to_number() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -2372,9 +2381,9 @@ fn test_mcp_tool_integer_normalized_to_number() { ); } -#[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 = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -2432,9 +2441,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 = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -2657,9 +2666,9 @@ Examples of valid command strings: assert_eq!(description, &expected); } -#[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 = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); @@ -2767,9 +2776,9 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { ); } -#[test] -fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { - let config = test_config(); +#[tokio::test] +async fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); @@ -2798,9 +2807,9 @@ fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { ); } -#[test] -fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { - let config = test_config(); +#[tokio::test] +async fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); @@ -2850,8 +2859,8 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { ); } -#[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); @@ -2861,12 +2870,13 @@ fn code_mode_only_restricts_model_tools_to_exec_tools() { &features, Some(WebSearchMode::Live), &["exec", "wait"], - ); + ) + .await; } -#[test] -fn code_mode_only_exec_description_includes_full_nested_tool_details() { - let config = test_config(); +#[tokio::test] +async fn code_mode_only_exec_description_includes_full_nested_tool_details() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); @@ -2897,9 +2907,9 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() { assert!(description.contains("### `view_image` (`view_image`)")); } -#[test] -fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() { - let config = test_config(); +#[tokio::test] +async fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() { + let config = test_config().await; let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 7cc3f78401..78a441d330 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -2,9 +2,9 @@ use std::sync::Arc; use crate::ExecServerClient; use crate::ExecServerError; +use crate::LOCAL_FS; use crate::RemoteExecServerConnectArgs; use crate::file_system::ExecutorFileSystem; -use crate::local_file_system::LocalFileSystem; use crate::local_process::LocalProcess; use crate::process::ExecProcess; use crate::remote_file_system::RemoteFileSystem; @@ -101,7 +101,7 @@ impl Environment { if let Some(client) = self.remote_exec_server_client.clone() { Arc::new(RemoteFileSystem::new(client)) } else { - Arc::new(LocalFileSystem) + Arc::new(LOCAL_FS) } } } diff --git a/codex-rs/exec-server/src/file_system.rs b/codex-rs/exec-server/src/file_system.rs index 35c2243f8e..3c0b11a1a1 100644 --- a/codex-rs/exec-server/src/file_system.rs +++ b/codex-rs/exec-server/src/file_system.rs @@ -39,6 +39,11 @@ pub type FileSystemResult = io::Result; pub trait ExecutorFileSystem: Send + Sync { async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult>; + async fn read_to_string(&self, path: &AbsolutePathBuf) -> FileSystemResult { + let bytes = self.read_file(path).await?; + String::from_utf8(bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) + } + async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec) -> FileSystemResult<()>; async fn create_directory( diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 68ff9f6549..353f5a9fc2 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -39,6 +39,7 @@ pub use file_system::FileMetadata; pub use file_system::FileSystemResult; pub use file_system::ReadDirectoryEntry; pub use file_system::RemoveOptions; +pub use local_file_system::LocalFileSystem; pub use process::ExecProcess; pub use process::ExecServerEvent; pub use protocol::ExecExitedNotification; @@ -58,3 +59,5 @@ pub use server::DEFAULT_LISTEN_URL; pub use server::ExecServerListenUrlParseError; pub use server::run_main; pub use server::run_main_with_listen_url; + +pub const LOCAL_FS: LocalFileSystem = LocalFileSystem; diff --git a/codex-rs/exec-server/src/local_file_system.rs b/codex-rs/exec-server/src/local_file_system.rs index fba7efa306..dbc404f056 100644 --- a/codex-rs/exec-server/src/local_file_system.rs +++ b/codex-rs/exec-server/src/local_file_system.rs @@ -18,7 +18,7 @@ use crate::RemoveOptions; const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024; #[derive(Clone, Default)] -pub(crate) struct LocalFileSystem; +pub struct LocalFileSystem; #[async_trait] impl ExecutorFileSystem for LocalFileSystem { diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index 127ac9f667..3c3a9a13fd 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -1,5 +1,6 @@ use codex_core::AuthManager; use codex_core::config::Config; +use codex_core::git_info::resolve_root_git_project_for_trust; #[cfg(target_os = "windows")] use codex_core::windows_sandbox::WindowsSandboxLevelExt; #[cfg(target_os = "windows")] @@ -74,7 +75,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, @@ -117,15 +118,18 @@ impl OnboardingScreen { WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled; #[cfg(not(target_os = "windows"))] 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, codex_home, + cwd, + trust_target, show_windows_create_sandbox_hint, should_quit: false, selection: None, - highlighted, + highlighted: TrustDirectorySelection::Trust, error: None, })) } @@ -398,7 +402,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 06c73cc2db..dd905f9e68 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 codex_core::config::set_project_trust_level; -use codex_core::git_info::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,13 +142,18 @@ 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()); - if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) { + if let Err(e) = + set_project_trust_level(&self.codex_home, &self.trust_target, TrustLevel::Trusted) + { tracing::error!("Failed to set project trusted: {e:?}"); - self.error = Some(format!("Failed to set trust for {}: {e}", target.display())); + self.error = Some(format!( + "Failed to set trust for {}: {e}", + self.trust_target.display() + )); + return; } + self.error = None; self.selection = Some(TrustDirectorySelection::Trust); } @@ -182,6 +187,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 +213,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, diff --git a/codex-rs/tui_app_server/src/onboarding/onboarding_screen.rs b/codex-rs/tui_app_server/src/onboarding/onboarding_screen.rs index b6afd2cb98..15e4956f69 100644 --- a/codex-rs/tui_app_server/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui_app_server/src/onboarding/onboarding_screen.rs @@ -2,6 +2,7 @@ use codex_app_server_client::AppServerEvent; use codex_app_server_client::AppServerRequestHandle; use codex_app_server_protocol::ServerNotification; use codex_core::config::Config; +use codex_core::git_info::resolve_root_git_project_for_trust; #[cfg(target_os = "windows")] use codex_core::windows_sandbox::WindowsSandboxLevelExt; #[cfg(target_os = "windows")] @@ -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, @@ -124,15 +125,18 @@ impl OnboardingScreen { WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled; #[cfg(not(target_os = "windows"))] 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, codex_home, + cwd, + trust_target, show_windows_create_sandbox_hint, should_quit: false, selection: None, - highlighted, + highlighted: TrustDirectorySelection::Trust, error: None, })) } @@ -429,7 +433,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_app_server/src/onboarding/trust_directory.rs b/codex-rs/tui_app_server/src/onboarding/trust_directory.rs index 06c73cc2db..dd905f9e68 100644 --- a/codex-rs/tui_app_server/src/onboarding/trust_directory.rs +++ b/codex-rs/tui_app_server/src/onboarding/trust_directory.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use codex_core::config::set_project_trust_level; -use codex_core::git_info::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,13 +142,18 @@ 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()); - if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) { + if let Err(e) = + set_project_trust_level(&self.codex_home, &self.trust_target, TrustLevel::Trusted) + { tracing::error!("Failed to set project trusted: {e:?}"); - self.error = Some(format!("Failed to set trust for {}: {e}", target.display())); + self.error = Some(format!( + "Failed to set trust for {}: {e}", + self.trust_target.display() + )); + return; } + self.error = None; self.selection = Some(TrustDirectorySelection::Trust); } @@ -182,6 +187,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 +213,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,