mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
d97bad1272
commit
bd61737e8a
@@ -971,6 +971,7 @@ mod tests {
|
||||
match ConfigBuilder::default().build().await {
|
||||
Ok(config) => config,
|
||||
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
|
||||
.await
|
||||
.expect("default config should load"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2442,6 +2442,7 @@ impl CodexMessageProcessor {
|
||||
))
|
||||
{
|
||||
let trust_target = resolve_root_git_project_for_trust(config.cwd.as_path())
|
||||
.await
|
||||
.unwrap_or_else(|| config.cwd.to_path_buf());
|
||||
let cli_overrides_with_trust;
|
||||
let cli_overrides_for_reload = if let Err(err) =
|
||||
|
||||
@@ -716,6 +716,7 @@ mod tests {
|
||||
match ConfigBuilder::default().build().await {
|
||||
Ok(config) => config,
|
||||
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
|
||||
.await
|
||||
.expect("default config should load"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,12 +432,14 @@ pub async fn run_main_with_transport(
|
||||
Err(err) => {
|
||||
let message = config_warning_from_error("Invalid configuration; using defaults.", &err);
|
||||
config_warnings.push(message);
|
||||
Config::load_default_with_cli_overrides(cli_kv_overrides.clone()).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading default config after config error: {e}"),
|
||||
)
|
||||
})?
|
||||
Config::load_default_with_cli_overrides(cli_kv_overrides.clone())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading default config after config error: {e}"),
|
||||
)
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -717,6 +717,7 @@ model_reasoning_effort = "high"
|
||||
|
||||
let config_toml = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
|
||||
let trusted_root = resolve_root_git_project_for_trust(workspace.path())
|
||||
.await
|
||||
.unwrap_or_else(|| workspace.path().to_path_buf());
|
||||
assert!(config_toml.contains(&persisted_trust_path(&trusted_root)));
|
||||
assert!(config_toml.contains("trust_level = \"trusted\""));
|
||||
@@ -753,8 +754,9 @@ async fn thread_start_with_nested_git_cwd_trusts_repo_root() -> Result<()> {
|
||||
.await??;
|
||||
|
||||
let config_toml = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
|
||||
let trusted_root =
|
||||
resolve_root_git_project_for_trust(&nested).expect("git root should resolve");
|
||||
let trusted_root = resolve_root_git_project_for_trust(&nested)
|
||||
.await
|
||||
.expect("git root should resolve");
|
||||
assert!(config_toml.contains(&persisted_trust_path(&trusted_root)));
|
||||
assert!(!config_toml.contains(&persisted_trust_path(&nested)));
|
||||
|
||||
|
||||
@@ -596,7 +596,7 @@ pub struct GhostSnapshotToml {
|
||||
|
||||
impl ConfigToml {
|
||||
/// Derive the effective sandbox policy from the configuration.
|
||||
pub fn derive_sandbox_policy(
|
||||
pub async fn derive_sandbox_policy(
|
||||
&self,
|
||||
sandbox_mode_override: Option<SandboxMode>,
|
||||
profile_sandbox_mode: Option<SandboxMode>,
|
||||
@@ -610,11 +610,13 @@ impl ConfigToml {
|
||||
let resolved_sandbox_mode = sandbox_mode_override
|
||||
.or(profile_sandbox_mode)
|
||||
.or(self.sandbox_mode)
|
||||
.or_else(|| {
|
||||
.or(if sandbox_mode_was_explicit {
|
||||
None
|
||||
} else {
|
||||
// If no sandbox_mode is set but this directory has a trust decision,
|
||||
// default to workspace-write except on unsandboxed Windows where we
|
||||
// default to read-only.
|
||||
self.get_active_project(resolved_cwd).and_then(|p| {
|
||||
self.get_active_project(resolved_cwd).await.and_then(|p| {
|
||||
if p.is_trusted() || p.is_untrusted() {
|
||||
if cfg!(target_os = "windows")
|
||||
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
|
||||
@@ -676,7 +678,8 @@ impl ConfigToml {
|
||||
|
||||
/// Resolves the cwd to an existing project, or returns None if ConfigToml
|
||||
/// does not contain a project corresponding to cwd or a git repo for cwd
|
||||
pub fn get_active_project(&self, resolved_cwd: &Path) -> Option<ProjectConfig> {
|
||||
pub async fn get_active_project(&self, resolved_cwd: &Path) -> Option<ProjectConfig> {
|
||||
let repo_root = resolve_root_git_project_for_trust(resolved_cwd).await;
|
||||
let projects = self.projects.clone().unwrap_or_default();
|
||||
|
||||
let resolved_cwd_key = project_trust_key(resolved_cwd);
|
||||
@@ -691,8 +694,8 @@ impl ConfigToml {
|
||||
// If cwd lives inside a git repo/worktree, check whether the root git project
|
||||
// (the primary repository working directory) is trusted. This lets
|
||||
// worktrees inherit trust from the main project.
|
||||
if let Some(repo_root) = resolve_root_git_project_for_trust(resolved_cwd) {
|
||||
let repo_root_key = project_trust_key(repo_root.as_path());
|
||||
if let Some(repo_root) = repo_root.as_deref() {
|
||||
let repo_root_key = project_trust_key(repo_root);
|
||||
let repo_root_raw_key = repo_root.to_string_lossy().to_string();
|
||||
if let Some(project_config_for_root) = projects
|
||||
.get(&repo_root_key)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(¤t_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(¤t_group, &group, entries).await {
|
||||
sections.push(section);
|
||||
}
|
||||
}
|
||||
(!sections.is_empty()).then(|| sections.join("\n\n"))
|
||||
}
|
||||
|
||||
@@ -306,11 +307,11 @@ pub(crate) fn truncate_realtime_text_to_token_budget(text: &str, budget_tokens:
|
||||
}
|
||||
}
|
||||
|
||||
fn build_workspace_section_with_user_root(
|
||||
async fn build_workspace_section_with_user_root(
|
||||
cwd: &Path,
|
||||
user_root: Option<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())
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -618,7 +618,7 @@ async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option<String> {
|
||||
/// `[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<PathBuf> {
|
||||
pub async fn resolve_root_git_project_for_trust(cwd: &Path) -> Option<PathBuf> {
|
||||
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() {
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(super) async fn test_config() -> Config {
|
||||
.keep();
|
||||
let mut config =
|
||||
Config::load_default_with_cli_overrides_for_codex_home(codex_home.clone(), Vec::new())
|
||||
.await
|
||||
.expect("config");
|
||||
config.codex_home = codex_home.abs();
|
||||
config.sqlite_home = codex_home.clone();
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_app_server_client::AppServerEvent;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -77,7 +78,7 @@ pub(crate) struct OnboardingResult {
|
||||
}
|
||||
|
||||
impl OnboardingScreen {
|
||||
pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
||||
pub(crate) async fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
||||
let OnboardingScreenArgs {
|
||||
show_trust_screen,
|
||||
show_login_screen,
|
||||
@@ -122,8 +123,12 @@ impl OnboardingScreen {
|
||||
let show_windows_create_sandbox_hint = false;
|
||||
let highlighted = TrustDirectorySelection::Trust;
|
||||
if show_trust_screen {
|
||||
let trust_target = resolve_root_git_project_for_trust(&cwd)
|
||||
.await
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
|
||||
cwd,
|
||||
trust_target,
|
||||
codex_home,
|
||||
show_windows_create_sandbox_hint,
|
||||
should_quit: false,
|
||||
@@ -452,7 +457,7 @@ pub(crate) async fn run_onboarding_app(
|
||||
) -> Result<OnboardingResult> {
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::legacy_core::config::set_project_trust_level;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
@@ -27,6 +26,7 @@ use super::onboarding_screen::StepState;
|
||||
pub(crate) struct TrustDirectoryWidget {
|
||||
pub codex_home: PathBuf,
|
||||
pub cwd: PathBuf,
|
||||
pub trust_target: PathBuf,
|
||||
pub show_windows_create_sandbox_hint: bool,
|
||||
pub should_quit: bool,
|
||||
pub selection: Option<TrustDirectorySelection>,
|
||||
@@ -142,8 +142,7 @@ impl StepStateProvider for TrustDirectoryWidget {
|
||||
|
||||
impl TrustDirectoryWidget {
|
||||
fn handle_trust(&mut self) {
|
||||
let target =
|
||||
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
|
||||
let target = self.trust_target.clone();
|
||||
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) {
|
||||
tracing::error!("Failed to set project trusted: {e:?}");
|
||||
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
|
||||
@@ -182,6 +181,7 @@ mod tests {
|
||||
let mut widget = TrustDirectoryWidget {
|
||||
codex_home: codex_home.path().to_path_buf(),
|
||||
cwd: PathBuf::from("."),
|
||||
trust_target: PathBuf::from("."),
|
||||
show_windows_create_sandbox_hint: false,
|
||||
should_quit: false,
|
||||
selection: None,
|
||||
@@ -207,6 +207,7 @@ mod tests {
|
||||
let widget = TrustDirectoryWidget {
|
||||
codex_home: codex_home.path().to_path_buf(),
|
||||
cwd: PathBuf::from("/workspace/project"),
|
||||
trust_target: PathBuf::from("/workspace/project"),
|
||||
show_windows_create_sandbox_hint: false,
|
||||
should_quit: false,
|
||||
selection: None,
|
||||
|
||||
Reference in New Issue
Block a user