Async config loading (#18022)

Parts of config will come from executor. Prepare for that by making
config loading methods async.
This commit is contained in:
pakrym-oai
2026-04-15 19:18:38 -07:00
committed by GitHub
parent d97bad1272
commit bd61737e8a
25 changed files with 624 additions and 471 deletions
+5 -3
View File
@@ -78,7 +78,8 @@ async fn apply_role_to_config_inner(
role_layer_toml,
preserve_current_profile,
preserve_current_provider,
)?;
)
.await?;
Ok(())
}
@@ -150,7 +151,7 @@ fn preservation_policy(config: &Config, role_layer_toml: &TomlValue) -> (bool, b
mod reload {
use super::*;
pub(super) fn build_next_config(
pub(super) async fn build_next_config(
config: &Config,
role_layer_toml: TomlValue,
preserve_current_profile: bool,
@@ -171,7 +172,8 @@ mod reload {
reload_overrides(config, preserve_current_provider),
config.codex_home.clone(),
config_layer_stack,
)?;
)
.await?;
if preserve_current_profile {
next_config.active_profile = config.active_profile.clone();
}
+15 -15
View File
@@ -457,8 +457,8 @@ fn numbered_mcp_tools(count: usize) -> HashMap<String, ToolInfo> {
.collect()
}
fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
let config = test_config();
async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
let config = test_config().await;
let model_info = ModelsManager::construct_model_info_offline_for_tests(
"gpt-5-codex",
&config.to_models_manager_config(),
@@ -870,7 +870,7 @@ async fn get_base_instructions_no_user_content() {
];
let (session, _turn_context) = make_session_and_context().await;
let config = test_config();
let config = test_config().await;
for test_case in test_cases {
let model_info = model_info_for_slug(test_case.slug, &config);
@@ -1035,10 +1035,10 @@ fn collect_explicit_app_ids_from_skill_items_skips_plain_mentions_with_skill_con
assert_eq!(connector_ids, HashSet::<String>::new());
}
#[test]
fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
let config = test_config();
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
#[tokio::test]
async fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
let exposure = build_mcp_tool_exposure(
@@ -1057,10 +1057,10 @@ fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
assert!(exposure.deferred_tools.is_none());
}
#[test]
fn mcp_tool_exposure_searches_large_effective_tool_sets() {
let config = test_config();
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
#[tokio::test]
async fn mcp_tool_exposure_searches_large_effective_tool_sets() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
let exposure = build_mcp_tool_exposure(
@@ -1083,10 +1083,10 @@ fn mcp_tool_exposure_searches_large_effective_tool_sets() {
assert_eq!(deferred_tool_names, expected_tool_names);
}
#[test]
fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overlap() {
let config = test_config();
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
#[tokio::test]
async fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overlap() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mut mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
mcp_tools.extend([(
"mcp__codex_apps__calendar_create_event".to_string(),
File diff suppressed because it is too large Load Diff
+24 -13
View File
@@ -169,13 +169,14 @@ fn resolve_mcp_oauth_credentials_store_mode(
}
#[cfg(test)]
pub(crate) fn test_config() -> Config {
pub(crate) async fn test_config() -> Config {
let codex_home = tempfile::tempdir().expect("create temp dir");
Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("temp dir should resolve"),
)
.await
.expect("load default test config")
}
@@ -726,6 +727,7 @@ impl ConfigBuilder {
codex_home,
config_layer_stack,
)
.await
}
#[cfg(test)]
@@ -786,7 +788,7 @@ impl Config {
}
/// Load a default configuration when user config files are invalid.
pub fn load_default_with_cli_overrides(
pub async fn load_default_with_cli_overrides(
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<Self> {
let codex_home = find_codex_home()?;
@@ -794,11 +796,12 @@ impl Config {
codex_home.to_path_buf(),
cli_overrides,
)
.await
}
/// Load a default configuration for a specific Codex home without reading
/// user, project, or system config layers.
pub fn load_default_with_cli_overrides_for_codex_home(
pub async fn load_default_with_cli_overrides_for_codex_home(
codex_home: PathBuf,
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<Self> {
@@ -818,6 +821,7 @@ impl Config {
codex_home,
ConfigLayerStack::default(),
)
.await
}
/// This is a secondary way of creating [Config], which is appropriate when
@@ -1413,22 +1417,24 @@ pub(crate) fn resolve_web_search_mode_for_turn(
impl Config {
#[cfg(test)]
fn load_from_base_config_with_overrides(
async fn load_from_base_config_with_overrides(
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
) -> std::io::Result<Self> {
// Note this ignores requirements.toml enforcement for tests.
let config_layer_stack = ConfigLayerStack::default();
Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack)
Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack).await
}
pub(crate) fn load_config_with_layer_stack(
pub(crate) async fn load_config_with_layer_stack(
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
config_layer_stack: ConfigLayerStack,
) -> std::io::Result<Self> {
// Keep the large config-construction future off small test thread stacks.
Box::pin(async move {
validate_model_providers(&cfg.model_providers)
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
// Ensure that every field of ConfigRequirements is applied to the final
@@ -1547,6 +1553,7 @@ impl Config {
.collect();
let active_project = cfg
.get_active_project(resolved_cwd.as_path())
.await
.unwrap_or(ProjectConfig { trust_level: None });
let permission_config_syntax = resolve_permission_config_syntax(
&config_layer_stack,
@@ -1636,13 +1643,15 @@ impl Config {
)
} else {
let configured_network_proxy_config = NetworkProxyConfig::default();
let mut sandbox_policy = cfg.derive_sandbox_policy(
sandbox_mode,
config_profile.sandbox_mode,
windows_sandbox_level,
resolved_cwd.as_path(),
Some(&constrained_sandbox_policy),
);
let mut sandbox_policy = cfg
.derive_sandbox_policy(
sandbox_mode,
config_profile.sandbox_mode,
windows_sandbox_level,
resolved_cwd.as_path(),
Some(&constrained_sandbox_policy),
)
.await;
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
for path in &additional_writable_roots {
if !writable_roots.iter().any(|existing| existing == path) {
@@ -2207,6 +2216,8 @@ impl Config {
},
};
Ok(config)
})
.await
}
fn load_instructions(codex_dir: Option<&AbsolutePathBuf>) -> Option<LoadedUserInstructions> {
@@ -24,8 +24,8 @@ fn normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths() {
assert_eq!(parsed, PathBuf::from(r"D:\c\x\worktrees\2508\swift-base"));
}
#[test]
fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> {
#[tokio::test]
async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let cwd = temp_dir.path().join("workspace");
let codex_home = temp_dir.path().join(".codex");
@@ -64,7 +64,8 @@ fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()>
..Default::default()
},
AbsolutePathBuf::from_absolute_path(&codex_home)?,
)?;
)
.await?;
let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?;
let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?;
+1 -1
View File
@@ -650,7 +650,7 @@ async fn project_trust_context(
let projects = project_trust_config.projects.unwrap_or_default();
let project_root_key = project_trust_key(project_root.as_path());
let repo_root = resolve_root_git_project_for_trust(cwd.as_path());
let repo_root = resolve_root_git_project_for_trust(cwd.as_path()).await;
let repo_root_key = repo_root.as_ref().map(|root| project_trust_key(root));
let projects_trust = projects
+30 -17
View File
@@ -426,10 +426,14 @@ async fn test_get_git_working_tree_state_branch_fallback() {
assert_eq!(state.sha, GitSha::new(&remote_sha));
}
#[test]
fn resolve_root_git_project_for_trust_returns_none_outside_repo() {
#[tokio::test]
async fn resolve_root_git_project_for_trust_returns_none_outside_repo() {
let tmp = TempDir::new().expect("tempdir");
assert!(resolve_root_git_project_for_trust(tmp.path()).is_none());
assert!(
resolve_root_git_project_for_trust(tmp.path())
.await
.is_none()
);
}
#[tokio::test]
@@ -439,12 +443,15 @@ async fn resolve_root_git_project_for_trust_regular_repo_returns_repo_root() {
let expected = std::fs::canonicalize(&repo_path).unwrap();
assert_eq!(
resolve_root_git_project_for_trust(&repo_path),
resolve_root_git_project_for_trust(&repo_path).await,
Some(expected.clone())
);
let nested = repo_path.join("sub/dir");
std::fs::create_dir_all(&nested).unwrap();
assert_eq!(resolve_root_git_project_for_trust(&nested), Some(expected));
assert_eq!(
resolve_root_git_project_for_trust(&nested).await,
Some(expected)
);
}
#[tokio::test]
@@ -467,18 +474,20 @@ async fn resolve_root_git_project_for_trust_detects_worktree_and_returns_main_ro
.expect("git worktree add");
let expected = std::fs::canonicalize(&repo_path).ok();
let got =
resolve_root_git_project_for_trust(&wt_root).and_then(|p| std::fs::canonicalize(p).ok());
let got = resolve_root_git_project_for_trust(&wt_root)
.await
.and_then(|p| std::fs::canonicalize(p).ok());
assert_eq!(got, expected);
let nested = wt_root.join("nested/sub");
std::fs::create_dir_all(&nested).unwrap();
let got_nested =
resolve_root_git_project_for_trust(&nested).and_then(|p| std::fs::canonicalize(p).ok());
let got_nested = resolve_root_git_project_for_trust(&nested)
.await
.and_then(|p| std::fs::canonicalize(p).ok());
assert_eq!(got_nested, expected);
}
#[test]
fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_command() {
#[tokio::test]
async fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_command() {
let tmp = TempDir::new().expect("tempdir");
let repo_root = tmp.path().join("repo");
let common_dir = repo_root.join(".git");
@@ -495,17 +504,17 @@ fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_comma
let expected = std::fs::canonicalize(&repo_root).unwrap();
assert_eq!(
resolve_root_git_project_for_trust(&worktree_root),
resolve_root_git_project_for_trust(&worktree_root).await,
Some(expected.clone())
);
assert_eq!(
resolve_root_git_project_for_trust(&worktree_root.join("nested")),
resolve_root_git_project_for_trust(&worktree_root.join("nested")).await,
Some(expected)
);
}
#[test]
fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() {
#[tokio::test]
async fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() {
let tmp = TempDir::new().expect("tempdir");
let proj = tmp.path().join("proj");
std::fs::create_dir_all(proj.join("nested")).unwrap();
@@ -520,8 +529,12 @@ fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() {
)
.unwrap();
assert!(resolve_root_git_project_for_trust(&proj).is_none());
assert!(resolve_root_git_project_for_trust(&proj.join("nested")).is_none());
assert!(resolve_root_git_project_for_trust(&proj).await.is_none());
assert!(
resolve_root_git_project_for_trust(&proj.join("nested"))
.await
.is_none()
);
}
#[tokio::test]
+6 -6
View File
@@ -823,9 +823,9 @@ async fn interrupt_and_drain_turn(codex: &Codex) -> anyhow::Result<()> {
mod tests {
use super::*;
#[test]
fn guardian_review_session_config_change_invalidates_cached_session() {
let parent_config = crate::config::test_config();
#[tokio::test]
async fn guardian_review_session_config_change_invalidates_cached_session() {
let parent_config = crate::config::test_config().await;
let cached_spawn_config = build_guardian_review_session_config(
&parent_config,
/*live_network_config*/ None,
@@ -855,9 +855,9 @@ mod tests {
);
}
#[test]
fn guardian_review_session_config_disables_hooks() {
let mut parent_config = crate::config::test_config();
#[tokio::test]
async fn guardian_review_session_config_disables_hooks() {
let mut parent_config = crate::config::test_config().await;
parent_config
.features
.enable(Feature::CodexHooks)
+24 -21
View File
@@ -1332,8 +1332,8 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() ->
Ok(())
}
#[test]
fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow::Result<()> {
#[tokio::test]
async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow::Result<()> {
const TEST_STACK_SIZE_BYTES: usize = 2 * 1024 * 1024;
let handle =
@@ -1569,9 +1569,9 @@ fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow:
)),
}
}
#[test]
fn guardian_review_session_config_preserves_parent_network_proxy() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_preserves_parent_network_proxy() {
let mut parent_config = test_config().await;
let network = NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig::default(),
Some(NetworkConstraints {
@@ -1616,9 +1616,9 @@ fn guardian_review_session_config_preserves_parent_network_proxy() {
);
}
#[test]
fn guardian_review_session_config_overrides_parent_developer_instructions() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_overrides_parent_developer_instructions() {
let mut parent_config = test_config().await;
parent_config.developer_instructions =
Some("parent or managed config should not replace guardian policy".to_string());
@@ -1636,9 +1636,9 @@ fn guardian_review_session_config_overrides_parent_developer_instructions() {
);
}
#[test]
fn guardian_review_session_config_uses_live_network_proxy_state() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_uses_live_network_proxy_state() {
let mut parent_config = test_config().await;
let mut parent_network = NetworkProxyConfig::default();
parent_network.network.enabled = true;
parent_network
@@ -1680,9 +1680,9 @@ fn guardian_review_session_config_uses_live_network_proxy_state() {
);
}
#[test]
fn guardian_review_session_config_rejects_pinned_collab_feature() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_rejects_pinned_collab_feature() {
let mut parent_config = test_config().await;
parent_config.features = ManagedFeatures::from_configured(
parent_config.features.get().clone(),
Some(Sourced {
@@ -1708,9 +1708,9 @@ fn guardian_review_session_config_rejects_pinned_collab_feature() {
);
}
#[test]
fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_slug() {
let mut parent_config = test_config();
#[tokio::test]
async fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_slug() {
let mut parent_config = test_config().await;
parent_config.model = Some("configured-model".to_string());
let guardian_config = build_guardian_review_session_config_for_test(
@@ -1724,8 +1724,8 @@ fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_
assert_eq!(guardian_config.model, Some("active-model".to_string()));
}
#[test]
fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
#[tokio::test]
async fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
let codex_home = tempfile::tempdir().expect("create temp dir");
let workspace = tempfile::tempdir().expect("create temp dir");
let config_layer_stack = ConfigLayerStack::new(
@@ -1748,6 +1748,7 @@ fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
codex_home.abs(),
config_layer_stack,
)
.await
.expect("load config");
let guardian_config = build_guardian_review_session_config_for_test(
@@ -1766,8 +1767,9 @@ fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
);
}
#[test]
fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override() {
#[tokio::test]
async fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override()
{
let codex_home = tempfile::tempdir().expect("create temp dir");
let workspace = tempfile::tempdir().expect("create temp dir");
let config_layer_stack =
@@ -1782,6 +1784,7 @@ fn guardian_review_session_config_uses_default_guardian_policy_without_requireme
codex_home.abs(),
config_layer_stack,
)
.await
.expect("load config");
let guardian_config = build_guardian_review_session_config_for_test(
+2 -2
View File
@@ -468,7 +468,7 @@ mod phase2 {
impl DispatchHarness {
async fn new() -> Self {
let codex_home = tempfile::tempdir().expect("create temp codex home");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home =
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path())
.expect("codex home is absolute");
@@ -895,7 +895,7 @@ mod phase2 {
#[tokio::test]
async fn dispatch_marks_job_for_retry_when_spawn_agent_fails() {
let codex_home = tempfile::tempdir().expect("create temp codex home");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home =
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path())
.expect("codex home is absolute");
+1 -1
View File
@@ -106,7 +106,7 @@ mod tests {
async fn build_prompt_input_includes_context_and_user_message() {
let codex_home = tempfile::tempdir().expect("create codex home");
let cwd = tempfile::tempdir().expect("create cwd");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home =
AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("codex home is absolute");
config.cwd = AbsolutePathBuf::try_from(cwd.path().to_path_buf()).expect("absolute cwd");
+23 -19
View File
@@ -62,8 +62,8 @@ pub(crate) async fn build_realtime_startup_context(
let history = sess.clone_history().await;
let current_thread_section = build_current_thread_section(history.raw_items());
let recent_threads = load_recent_threads(sess).await;
let recent_work_section = build_recent_work_section(&cwd, &recent_threads);
let workspace_section = build_workspace_section_with_user_root(&cwd, home_dir());
let recent_work_section = build_recent_work_section(&cwd, &recent_threads).await;
let workspace_section = build_workspace_section_with_user_root(&cwd, home_dir()).await;
if current_thread_section.is_none()
&& recent_work_section.is_none()
@@ -144,16 +144,18 @@ async fn load_recent_threads(sess: &Session) -> Vec<StoredThread> {
}
}
fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Option<String> {
async fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Option<String> {
let mut groups: HashMap<PathBuf, Vec<&StoredThread>> = HashMap::new();
for entry in recent_threads {
let group =
resolve_root_git_project_for_trust(&entry.cwd).unwrap_or_else(|| entry.cwd.clone());
let group = resolve_root_git_project_for_trust(&entry.cwd)
.await
.unwrap_or_else(|| entry.cwd.clone());
groups.entry(group).or_default().push(entry);
}
let current_group =
resolve_root_git_project_for_trust(cwd).unwrap_or_else(|| cwd.to_path_buf());
let current_group = resolve_root_git_project_for_trust(cwd)
.await
.unwrap_or_else(|| cwd.to_path_buf());
let mut groups = groups.into_iter().collect::<Vec<_>>();
groups.sort_by(|(left_group, left_entries), (right_group, right_entries)| {
let left_latest = left_entries
@@ -178,14 +180,13 @@ fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Opt
))
});
let sections = groups
.into_iter()
.take(MAX_RECENT_WORK_GROUPS)
.filter_map(|(group, mut entries)| {
entries.sort_by_key(|entry| Reverse(entry.updated_at));
format_thread_group(&current_group, &group, entries)
})
.collect::<Vec<_>>();
let mut sections = Vec::new();
for (group, mut entries) in groups.into_iter().take(MAX_RECENT_WORK_GROUPS) {
entries.sort_by_key(|entry| Reverse(entry.updated_at));
if let Some(section) = format_thread_group(&current_group, &group, entries).await {
sections.push(section);
}
}
(!sections.is_empty()).then(|| sections.join("\n\n"))
}
@@ -306,11 +307,11 @@ pub(crate) fn truncate_realtime_text_to_token_budget(text: &str, budget_tokens:
}
}
fn build_workspace_section_with_user_root(
async fn build_workspace_section_with_user_root(
cwd: &Path,
user_root: Option<PathBuf>,
) -> Option<String> {
let git_root = resolve_root_git_project_for_trust(cwd);
let git_root = resolve_root_git_project_for_trust(cwd).await;
let cwd_tree = render_tree(cwd);
let git_root_tree = git_root
.as_ref()
@@ -466,13 +467,16 @@ fn format_startup_context_blob(body: &str, budget_tokens: usize) -> String {
}
}
fn format_thread_group(
async fn format_thread_group(
current_group: &Path,
group: &Path,
entries: Vec<&StoredThread>,
) -> Option<String> {
let latest = entries.first()?;
let group_label = if resolve_root_git_project_for_trust(latest.cwd.as_path()).is_some() {
let group_label = if resolve_root_git_project_for_trust(latest.cwd.as_path())
.await
.is_some()
{
format!("### Git repo: {}", group.display())
} else {
format!("### Directory: {}", group.display())
+12 -9
View File
@@ -188,30 +188,31 @@ fn startup_context_blob_is_wrapped_in_tags_and_fits_budget() {
assert!(wrapped.len().div_ceil(4) <= 200);
}
#[test]
fn workspace_section_requires_meaningful_structure() {
#[tokio::test]
async fn workspace_section_requires_meaningful_structure() {
let cwd = TempDir::new().expect("tempdir");
assert_eq!(
build_workspace_section_with_user_root(cwd.path(), /*user_root*/ None),
build_workspace_section_with_user_root(cwd.path(), /*user_root*/ None).await,
None
);
}
#[test]
fn workspace_section_includes_tree_when_entries_exist() {
#[tokio::test]
async fn workspace_section_includes_tree_when_entries_exist() {
let cwd = TempDir::new().expect("tempdir");
fs::create_dir(cwd.path().join("docs")).expect("create docs dir");
fs::write(cwd.path().join("README.md"), "hello").expect("write readme");
let section = build_workspace_section_with_user_root(cwd.path(), /*user_root*/ None)
.await
.expect("workspace section");
assert!(section.contains("Working directory tree:"));
assert!(section.contains("- docs/"));
assert!(section.contains("- README.md"));
}
#[test]
fn workspace_section_includes_user_root_tree_when_distinct() {
#[tokio::test]
async fn workspace_section_includes_user_root_tree_when_distinct() {
let root = TempDir::new().expect("tempdir");
let cwd = root.path().join("cwd");
let git_root = root.path().join("git");
@@ -225,14 +226,15 @@ fn workspace_section_includes_user_root_tree_when_distinct() {
fs::write(user_root.join(".zshrc"), "export TEST=1").expect("write home file");
let section = build_workspace_section_with_user_root(cwd.as_path(), Some(user_root))
.await
.expect("workspace section");
assert!(section.contains("User root tree:"));
assert!(section.contains("- code/"));
assert!(!section.contains("- .zshrc"));
}
#[test]
fn recent_work_section_groups_threads_by_cwd() {
#[tokio::test]
async fn recent_work_section_groups_threads_by_cwd() {
let root = TempDir::new().expect("tempdir");
let repo = root.path().join("repo");
let workspace_a = repo.join("workspace-a");
@@ -268,6 +270,7 @@ fn recent_work_section_groups_threads_by_cwd() {
let repo = fs::canonicalize(repo).expect("canonicalize repo");
let section = build_recent_work_section(current_cwd.as_path(), &recent_threads)
.await
.expect("recent work section");
assert!(section.contains(&format!("### Git repo: {}", repo.display())));
assert!(section.contains("Recent sessions: 2"));
+5 -5
View File
@@ -237,7 +237,7 @@ async fn ignores_session_prefix_messages_when_truncating() {
#[tokio::test]
async fn shutdown_all_threads_bounded_submits_shutdown_to_every_thread() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -279,7 +279,7 @@ async fn new_uses_configured_openai_provider_for_model_refresh() {
let models_mock = mount_models_once(&server, ModelsResponse { models: vec![] }).await;
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -422,7 +422,7 @@ fn mixed_response_and_legacy_user_event_history_is_mid_turn() {
#[tokio::test]
async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_history() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -525,7 +525,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
#[tokio::test]
async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
@@ -618,7 +618,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
#[tokio::test]
async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_source() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
+106 -93
View File
@@ -107,8 +107,8 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab
}))
}
fn search_capable_model_info() -> ModelInfo {
let config = test_config();
async fn search_capable_model_info() -> ModelInfo {
let config = test_config().await;
let mut model_info = construct_model_info_offline("gpt-5-codex", &config);
model_info.supports_search_tool = true;
model_info
@@ -216,8 +216,8 @@ fn find_namespace_function_tool<'a>(
.unwrap_or_else(|| panic!("expected tool {expected_namespace}{expected_name} in namespace"))
}
fn multi_agent_v2_tools_config() -> ToolsConfig {
let config = test_config();
async fn multi_agent_v2_tools_config() -> ToolsConfig {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Collab);
@@ -250,8 +250,8 @@ fn multi_agent_v2_spawn_agent_description(tools_config: &ToolsConfig) -> String
description.clone()
}
fn model_info_from_models_json(slug: &str) -> ModelInfo {
let config = test_config();
async fn model_info_from_models_json(slug: &str) -> ModelInfo {
let config = test_config().await;
let response = bundled_models_response()
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
let model = response
@@ -295,9 +295,9 @@ fn build_specs_with_unavailable_tools(
)
}
#[test]
fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
let mut model_info = model_info_from_models_json("gpt-5-codex");
#[tokio::test]
async fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
let mut model_info = model_info_from_models_json("gpt-5-codex").await;
model_info.shell_type = ConfigShellToolType::UnifiedExec;
let features = Features::with_defaults();
let available_models = Vec::new();
@@ -320,9 +320,9 @@ fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
assert_eq!(config.shell_type, expected_shell_type);
}
#[test]
fn get_memory_requires_feature_flag() {
let config = test_config();
#[tokio::test]
async fn get_memory_requires_feature_flag() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.disable(Feature::MemoryTool);
@@ -350,14 +350,14 @@ fn get_memory_requires_feature_flag() {
);
}
fn assert_model_tools(
async fn assert_model_tools(
model_slug: &str,
features: &Features,
web_search_mode: Option<WebSearchMode>,
expected_tools: &[&str],
) {
let _config = test_config();
let model_info = model_info_from_models_json(model_slug);
let _config = test_config().await;
let model_info = model_info_from_models_json(model_slug).await;
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
@@ -388,7 +388,7 @@ fn assert_model_tools(
assert_eq!(&tool_names, &expected_tools,);
}
fn assert_default_model_tools(
async fn assert_default_model_tools(
model_slug: &str,
features: &Features,
web_search_mode: Option<WebSearchMode>,
@@ -401,11 +401,11 @@ fn assert_default_model_tools(
vec![shell_tool]
};
expected.extend(expected_tail);
assert_model_tools(model_slug, features, web_search_mode, &expected);
assert_model_tools(model_slug, features, web_search_mode, &expected).await;
}
#[test]
fn test_build_specs_gpt5_codex_default() {
#[tokio::test]
async fn test_build_specs_gpt5_codex_default() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5-codex",
@@ -424,11 +424,12 @@ fn test_build_specs_gpt5_codex_default() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_build_specs_gpt51_codex_default() {
#[tokio::test]
async fn test_build_specs_gpt51_codex_default() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5.1-codex",
@@ -447,11 +448,12 @@ fn test_build_specs_gpt51_codex_default() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_build_specs_gpt5_codex_unified_exec_web_search() {
#[tokio::test]
async fn test_build_specs_gpt5_codex_unified_exec_web_search() {
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
assert_model_tools(
@@ -472,11 +474,12 @@ fn test_build_specs_gpt5_codex_unified_exec_web_search() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_build_specs_gpt51_codex_unified_exec_web_search() {
#[tokio::test]
async fn test_build_specs_gpt51_codex_unified_exec_web_search() {
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
assert_model_tools(
@@ -497,11 +500,12 @@ fn test_build_specs_gpt51_codex_unified_exec_web_search() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_gpt_5_1_codex_max_defaults() {
#[tokio::test]
async fn test_gpt_5_1_codex_max_defaults() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5.1-codex-max",
@@ -520,11 +524,12 @@ fn test_gpt_5_1_codex_max_defaults() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_codex_5_1_mini_defaults() {
#[tokio::test]
async fn test_codex_5_1_mini_defaults() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5.1-codex-mini",
@@ -543,11 +548,12 @@ fn test_codex_5_1_mini_defaults() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_gpt_5_defaults() {
#[tokio::test]
async fn test_gpt_5_defaults() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5",
@@ -565,11 +571,12 @@ fn test_gpt_5_defaults() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_gpt_5_1_defaults() {
#[tokio::test]
async fn test_gpt_5_1_defaults() {
let features = Features::with_defaults();
assert_default_model_tools(
"gpt-5.1",
@@ -588,11 +595,12 @@ fn test_gpt_5_1_defaults() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_gpt_5_1_codex_max_unified_exec_web_search() {
#[tokio::test]
async fn test_gpt_5_1_codex_max_unified_exec_web_search() {
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
assert_model_tools(
@@ -613,12 +621,13 @@ fn test_gpt_5_1_codex_max_unified_exec_web_search() {
"wait_agent",
"close_agent",
],
);
)
.await;
}
#[test]
fn test_build_specs_default_shell_present() {
let config = test_config();
#[tokio::test]
async fn test_build_specs_default_shell_present() {
let config = test_config().await;
let model_info = construct_model_info_offline("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -649,9 +658,9 @@ fn test_build_specs_default_shell_present() {
assert_contains_tool_names(&tools, &subset);
}
#[test]
fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
let config = test_config();
#[tokio::test]
async fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
let config = test_config().await;
let model_info = construct_model_info_offline("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -713,9 +722,10 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
);
}
#[test]
fn spawn_agent_description_omits_usage_hint_when_disabled() {
#[tokio::test]
async fn spawn_agent_description_omits_usage_hint_when_disabled() {
let tools_config = multi_agent_v2_tools_config()
.await
.with_spawn_agent_usage_hint(/*spawn_agent_usage_hint*/ false);
let description = multi_agent_v2_spawn_agent_description(&tools_config);
@@ -734,11 +744,13 @@ fn spawn_agent_description_omits_usage_hint_when_disabled() {
);
}
#[test]
fn spawn_agent_description_uses_configured_usage_hint_text() {
let tools_config = multi_agent_v2_tools_config().with_spawn_agent_usage_hint_text(Some(
/*spawn_agent_usage_hint_text*/ "Custom delegation guidance only.".to_string(),
));
#[tokio::test]
async fn spawn_agent_description_uses_configured_usage_hint_text() {
let tools_config = multi_agent_v2_tools_config()
.await
.with_spawn_agent_usage_hint_text(Some(
/*spawn_agent_usage_hint_text*/ "Custom delegation guidance only.".to_string(),
));
let description = multi_agent_v2_spawn_agent_description(&tools_config);
assert_regex_match(
@@ -757,9 +769,9 @@ fn spawn_agent_description_uses_configured_usage_hint_text() {
);
}
#[test]
fn tool_suggest_requires_apps_and_plugins_features() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn tool_suggest_requires_apps_and_plugins_features() {
let model_info = search_capable_model_info().await;
let discoverable_tools = Some(vec![discoverable_connector(
"connector_2128aebfecb84f64a069897515042a44",
"Google Calendar",
@@ -804,9 +816,9 @@ fn tool_suggest_requires_apps_and_plugins_features() {
}
}
#[test]
fn search_tool_description_handles_no_enabled_mcp_tools() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_description_handles_no_enabled_mcp_tools() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
features.enable(Feature::ToolSearch);
@@ -838,9 +850,9 @@ fn search_tool_description_handles_no_enabled_mcp_tools() {
assert!(!description.contains("{{source_descriptions}}"));
}
#[test]
fn search_tool_description_falls_back_to_connector_name_without_description() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_description_falls_back_to_connector_name_without_description() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
features.enable(Feature::ToolSearch);
@@ -889,9 +901,9 @@ fn search_tool_description_falls_back_to_connector_name_without_description() {
assert!(!description.contains("- Calendar:"));
}
#[test]
fn search_tool_registers_namespaced_mcp_tool_aliases() {
let model_info = search_capable_model_info();
#[tokio::test]
async fn search_tool_registers_namespaced_mcp_tool_aliases() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::Apps);
features.enable(Feature::ToolSearch);
@@ -974,9 +986,9 @@ fn search_tool_registers_namespaced_mcp_tool_aliases() {
assert!(registry.has_handler(&mcp_alias));
}
#[test]
fn direct_mcp_tools_register_namespaced_handlers() {
let config = test_config();
#[tokio::test]
async fn direct_mcp_tools_register_namespaced_handlers() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1011,9 +1023,9 @@ fn direct_mcp_tools_register_namespaced_handlers() {
assert!(!registry.has_handler(&ToolName::plain("mcp__test_server__echo")));
}
#[test]
fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
let config = test_config();
#[tokio::test]
async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1060,9 +1072,9 @@ fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
assert!(!registry.has_handler(&ToolName::plain("mcp__codex_apps__calendar_create_event")));
}
#[test]
fn test_mcp_tool_property_missing_type_defaults_to_string() {
let config = test_config();
#[tokio::test]
async fn test_mcp_tool_property_missing_type_defaults_to_string() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1123,9 +1135,9 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() {
);
}
#[test]
fn test_mcp_tool_preserves_integer_schema() {
let config = test_config();
#[tokio::test]
async fn test_mcp_tool_preserves_integer_schema() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1184,9 +1196,9 @@ fn test_mcp_tool_preserves_integer_schema() {
);
}
#[test]
fn test_mcp_tool_array_without_items_gets_default_string_items() {
let config = test_config();
#[tokio::test]
async fn test_mcp_tool_array_without_items_gets_default_string_items() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1249,9 +1261,9 @@ fn test_mcp_tool_array_without_items_gets_default_string_items() {
);
}
#[test]
fn test_mcp_tool_anyof_defaults_to_string() {
let config = test_config();
#[tokio::test]
async fn test_mcp_tool_anyof_defaults_to_string() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1318,9 +1330,9 @@ fn test_mcp_tool_anyof_defaults_to_string() {
);
}
#[test]
fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
let config = test_config();
#[tokio::test]
async fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
let config = test_config().await;
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
@@ -1433,8 +1445,8 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
);
}
#[test]
fn code_mode_only_restricts_model_tools_to_exec_tools() {
#[tokio::test]
async fn code_mode_only_restricts_model_tools_to_exec_tools() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
features.enable(Feature::CodeModeOnly);
@@ -1444,5 +1456,6 @@ fn code_mode_only_restricts_model_tools_to_exec_tools() {
&features,
Some(WebSearchMode::Live),
&["exec", "wait"],
);
)
.await;
}