use super::*;use codex_config::types::AppToolApproval;use codex_config::types::McpServerOAuthConfig;use codex_config::types::McpServerToolConfig;use codex_config::types::McpServerTransportConfig;use codex_config::types::SessionPickerViewMode;use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;use codex_protocol::config_types::ServiceTier;use codex_protocol::openai_models::ReasoningEffort;use pretty_assertions::assert_eq;#[cfg(unix)]use std::os::unix::fs::symlink;use tempfile::tempdir;use toml::Value as TomlValue;#[test]fn blocking_set_model_top_level() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); apply_blocking( codex_home, &[ConfigEdit::SetModel { model: Some("gpt-5.4".to_string()), effort: Some(ReasoningEffort::High), }], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"model = "gpt-5.4"model_reasoning_effort = "high""#; assert_eq!(contents, expected);}#[test]fn set_service_tier_saves_default_as_default() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .set_service_tier(Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents, "service_tier = \"default\"\n");}#[test]fn set_service_tier_saves_priority_as_fast() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .set_service_tier(Some(ServiceTier::Fast.request_value().to_string())) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents, "service_tier = \"fast\"\n");}#[test]fn set_service_tier_preserves_unknown_service_tier() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .set_service_tier(Some("experimental-tier-id".to_string())) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents, "service_tier = \"experimental-tier-id\"\n");}#[test]fn builder_with_edits_applies_custom_paths() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .with_edits(vec![ConfigEdit::SetPath { segments: vec!["enabled".to_string()], value: value(true), }]) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents, "enabled = true\n");}#[test]fn session_picker_view_edit_writes_root_tui_setting() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .with_edits([session_picker_view_edit(SessionPickerViewMode::Dense)]) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[tui]session_picker_view = "dense""#; assert_eq!(contents, expected);}#[test]fn keymap_binding_edit_writes_root_action_binding() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .with_edits([keymap_binding_edit("composer", "submit", "ctrl-enter")]) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[tui.keymap.composer]submit = "ctrl-enter""#; assert_eq!(contents, expected);}#[test]fn keymap_bindings_edit_writes_single_binding_as_string() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .with_edits([keymap_bindings_edit( "composer", "submit", &["ctrl-enter".to_string()], )]) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[tui.keymap.composer]submit = "ctrl-enter""#; assert_eq!(contents, expected);}#[test]fn keymap_bindings_edit_writes_multiple_bindings_as_array() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .with_edits([keymap_bindings_edit( "composer", "submit", &["enter".to_string(), "ctrl-enter".to_string()], )]) .apply_blocking() .expect("persist"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let value: TomlValue = toml::from_str(&raw).expect("parse config"); assert_eq!( value .get("tui") .and_then(|value| value.get("keymap")) .and_then(|value| value.get("composer")) .and_then(|value| value.get("submit")) .and_then(TomlValue::as_array) .map(|values| { values .iter() .filter_map(TomlValue::as_str) .collect::<Vec<_>>() }), Some(vec!["enter", "ctrl-enter"]) );}#[test]fn keymap_binding_edit_replaces_existing_binding_without_touching_profile() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"profile = "team"[tui.keymap.composer]submit = "enter"[profiles.team.tui.keymap.composer]submit = "shift-enter""#, ) .expect("seed config"); ConfigEditsBuilder::new(codex_home) .with_edits([keymap_binding_edit("composer", "submit", "ctrl-enter")]) .apply_blocking() .expect("persist"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let value: TomlValue = toml::from_str(&raw).expect("parse config"); assert_eq!( value .get("tui") .and_then(|value| value.get("keymap")) .and_then(|value| value.get("composer")) .and_then(|value| value.get("submit")) .and_then(TomlValue::as_str), Some("ctrl-enter") ); assert_eq!( value .get("profiles") .and_then(|value| value.get("team")) .and_then(|value| value.get("tui")) .and_then(|value| value.get("keymap")) .and_then(|value| value.get("composer")) .and_then(|value| value.get("submit")) .and_then(TomlValue::as_str), Some("shift-enter") );}#[test]fn keymap_binding_clear_edit_removes_root_action_binding_without_touching_profile() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"profile = "team"[tui.keymap.composer]submit = "enter"[profiles.team.tui.keymap.composer]submit = "shift-enter""#, ) .expect("seed config"); ConfigEditsBuilder::new(codex_home) .with_edits([keymap_binding_clear_edit("composer", "submit")]) .apply_blocking() .expect("persist"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let value: TomlValue = toml::from_str(&raw).expect("parse config"); assert_eq!( value .get("tui") .and_then(|value| value.get("keymap")) .and_then(|value| value.get("composer")) .and_then(|value| value.get("submit")), None ); assert_eq!( value .get("profiles") .and_then(|value| value.get("team")) .and_then(|value| value.get("tui")) .and_then(|value| value.get("keymap")) .and_then(|value| value.get("composer")) .and_then(|value| value.get("submit")) .and_then(TomlValue::as_str), Some("shift-enter") );}#[test]fn set_model_availability_nux_count_writes_shown_count() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); let shown_count = HashMap::from([("gpt-foo".to_string(), 4)]); ConfigEditsBuilder::new(codex_home) .set_model_availability_nux_count(&shown_count) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[tui.model_availability_nux]gpt-foo = 4"#; assert_eq!(contents, expected);}#[test]fn set_skill_config_writes_disabled_entry() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .with_edits([ConfigEdit::SetSkillConfig { path: PathBuf::from("/tmp/skills/demo/SKILL.md"), enabled: false, }]) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[[skills.config]]path = "/tmp/skills/demo/SKILL.md"enabled = false"#; assert_eq!(contents, expected);}#[test]fn set_skill_config_removes_entry_when_enabled() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[[skills.config]]path = "/tmp/skills/demo/SKILL.md"enabled = false"#, ) .expect("seed config"); ConfigEditsBuilder::new(codex_home) .with_edits([ConfigEdit::SetSkillConfig { path: PathBuf::from("/tmp/skills/demo/SKILL.md"), enabled: true, }]) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents, "");}#[test]fn set_skill_config_writes_name_selector_entry() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .with_edits([ConfigEdit::SetSkillConfigByName { name: "github:yeet".to_string(), enabled: false, }]) .apply_blocking() .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[[skills.config]]name = "github:yeet"enabled = false"#; assert_eq!(contents, expected);}#[test]fn blocking_set_model_ignores_inline_legacy_profile_contents() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); // Seed with inline tables for profiles to simulate common user config. std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"profile = "fast"profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }"#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::SetModel { model: Some("o4-mini".to_string()), effort: None, }], ) .expect("persist"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let value: TomlValue = toml::from_str(&raw).expect("parse config"); assert_eq!( value.get("model").and_then(TomlValue::as_str), Some("o4-mini") ); // Legacy profile values stay untouched when root settings are updated. let profiles_tbl = value .get("profiles") .and_then(|v| v.as_table()) .expect("profiles table"); let fast_tbl = profiles_tbl .get("fast") .and_then(|v| v.as_table()) .expect("fast table"); assert_eq!( fast_tbl.get("sandbox_mode").and_then(|v| v.as_str()), Some("strict") ); assert_eq!( fast_tbl.get("model").and_then(|v| v.as_str()), Some("gpt-4o") );}#[cfg(unix)]#[test]fn blocking_set_model_writes_through_symlink_chain() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); let target_dir = tempdir().expect("target dir"); let target_path = target_dir.path().join(CONFIG_TOML_FILE); let link_path = codex_home.join("config-link.toml"); let config_path = codex_home.join(CONFIG_TOML_FILE); symlink(&target_path, &link_path).expect("symlink link"); symlink("config-link.toml", &config_path).expect("symlink config"); apply_blocking( codex_home, &[ConfigEdit::SetModel { model: Some("gpt-5.4".to_string()), effort: Some(ReasoningEffort::High), }], ) .expect("persist"); let meta = std::fs::symlink_metadata(&config_path).expect("config metadata"); assert!(meta.file_type().is_symlink()); let contents = std::fs::read_to_string(&target_path).expect("read target"); let expected = r#"model = "gpt-5.4"model_reasoning_effort = "high""#; assert_eq!(contents, expected);}#[cfg(unix)]#[test]fn blocking_set_model_replaces_symlink_on_cycle() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); let link_a = codex_home.join("a.toml"); let link_b = codex_home.join("b.toml"); let config_path = codex_home.join(CONFIG_TOML_FILE); symlink("b.toml", &link_a).expect("symlink a"); symlink("a.toml", &link_b).expect("symlink b"); symlink("a.toml", &config_path).expect("symlink config"); apply_blocking( codex_home, &[ConfigEdit::SetModel { model: Some("gpt-5.4".to_string()), effort: None, }], ) .expect("persist"); let meta = std::fs::symlink_metadata(&config_path).expect("config metadata"); assert!(!meta.file_type().is_symlink()); let contents = std::fs::read_to_string(&config_path).expect("read config"); let expected = r#"model = "gpt-5.4""#; assert_eq!(contents, expected);}#[test]fn batch_write_table_upsert_preserves_inline_comments() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); let original = r#"approval_policy = "never"[mcp_servers.linear]name = "linear"# okurl = "https://linear.example"[mcp_servers.linear.http_headers]foo = "bar"[sandbox_workspace_write]# ok 3network_access = false"#; std::fs::write(codex_home.join(CONFIG_TOML_FILE), original).expect("seed config"); apply_blocking( codex_home, &[ ConfigEdit::SetPath { segments: vec![ "mcp_servers".to_string(), "linear".to_string(), "url".to_string(), ], value: value("https://linear.example/v2"), }, ConfigEdit::SetPath { segments: vec![ "sandbox_workspace_write".to_string(), "network_access".to_string(), ], value: value(true), }, ], ) .expect("apply"); let updated = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"approval_policy = "never"[mcp_servers.linear]name = "linear"# okurl = "https://linear.example/v2"[mcp_servers.linear.http_headers]foo = "bar"[sandbox_workspace_write]# ok 3network_access = true"#; assert_eq!(updated, expected);}#[test]fn blocking_clear_model_does_not_follow_legacy_active_profile() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"profile = "fast"profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }"#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::SetModel { model: None, effort: Some(ReasoningEffort::High), }], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"profile = "fast"profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }model_reasoning_effort = "high""#; assert_eq!(contents, expected);}#[test]fn blocking_set_model_does_not_follow_legacy_active_profile() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"profile = "team"[profiles.team]model_reasoning_effort = "low""#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::SetModel { model: Some("o5-preview".to_string()), effort: Some(ReasoningEffort::Minimal), }], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"profile = "team"model = "o5-preview"model_reasoning_effort = "minimal"[profiles.team]model_reasoning_effort = "low""#; assert_eq!(contents, expected);}#[test]fn blocking_set_hide_full_access_warning_preserves_table() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"# Global comment[notice]# keep meexisting = "value""#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::SetNoticeHideFullAccessWarning(true)], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"# Global comment[notice]# keep meexisting = "value"hide_full_access_warning = true"#; assert_eq!(contents, expected);}#[test]fn blocking_set_hide_rate_limit_model_nudge_preserves_table() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[notice]existing = "value""#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::SetNoticeHideRateLimitModelNudge(true)], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[notice]existing = "value"hide_rate_limit_model_nudge = true"#; assert_eq!(contents, expected);}#[test]fn blocking_set_hide_gpt5_1_migration_prompt_preserves_table() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[notice]existing = "value""#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::SetNoticeHideModelMigrationPrompt( "hide_gpt5_1_migration_prompt".to_string(), true, )], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[notice]existing = "value"hide_gpt5_1_migration_prompt = true"#; assert_eq!(contents, expected);}#[test]fn blocking_set_hide_gpt_5_1_codex_max_migration_prompt_preserves_table() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[notice]existing = "value""#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::SetNoticeHideModelMigrationPrompt( "hide_gpt-5.1-codex-max_migration_prompt".to_string(), true, )], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[notice]existing = "value""hide_gpt-5.1-codex-max_migration_prompt" = true"#; assert_eq!(contents, expected);}#[test]fn blocking_record_model_migration_seen_preserves_table() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[notice]existing = "value""#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::RecordModelMigrationSeen { from: "gpt-5.2".to_string(), to: "gpt-5.4".to_string(), }], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[notice]existing = "value"[notice.model_migrations]"gpt-5.2" = "gpt-5.4""#; assert_eq!(contents, expected);}#[test]fn blocking_set_hide_external_config_migration_prompt_home_preserves_table() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[notice]existing = "value""#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome( true, )], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[notice]existing = "value"[notice.external_config_migration_prompts]home = true"#; assert_eq!(contents, expected);}#[test]fn blocking_set_hide_external_config_migration_prompt_project_preserves_table() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[notice]existing = "value""#, ) .expect("seed"); apply_blocking( codex_home, &[ ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject( "/Users/alexsong/code/skills".to_string(), true, ), ], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[notice]existing = "value"[notice.external_config_migration_prompts.projects]"/Users/alexsong/code/skills" = true"#; assert_eq!(contents, expected);}#[test]fn blocking_set_external_config_migration_prompt_home_last_prompted_at_preserves_table() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[notice]existing = "value""#, ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(1_760_000_000)], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[notice]existing = "value"[notice.external_config_migration_prompts]home_last_prompted_at = 1760000000"#; assert_eq!(contents, expected);}#[test]fn blocking_set_external_config_migration_prompt_project_last_prompted_at_preserves_table() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[notice]existing = "value""#, ) .expect("seed"); apply_blocking( codex_home, &[ ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt( "/Users/alexsong/code/skills".to_string(), 1_760_000_000, ), ], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[notice]existing = "value"[notice.external_config_migration_prompts.project_last_prompted_at]"/Users/alexsong/code/skills" = 1760000000"#; assert_eq!(contents, expected);}#[test]fn blocking_replace_mcp_servers_round_trips() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); let mut servers = BTreeMap::new(); servers.insert( "stdio".to_string(), McpServerConfig { transport: McpServerTransportConfig::Stdio { command: "cmd".to_string(), args: vec!["--flag".to_string()], env: Some( [ ("B".to_string(), "2".to_string()), ("A".to_string(), "1".to_string()), ] .into_iter() .collect(), ), env_vars: vec!["FOO".into()], cwd: None, }, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: true, required: false, supports_parallel_tool_calls: true, disabled_reason: None, startup_timeout_sec: None, tool_timeout_sec: None, default_tools_approval_mode: None, enabled_tools: Some(vec!["one".to_string(), "two".to_string()]), disabled_tools: None, scopes: None, oauth: None, oauth_resource: None, tools: HashMap::new(), }, ); servers.insert( "http".to_string(), McpServerConfig { transport: McpServerTransportConfig::StreamableHttp { url: "https://example.com".to_string(), bearer_token_env_var: Some("TOKEN".to_string()), http_headers: Some( [("Z-Header".to_string(), "z".to_string())] .into_iter() .collect(), ), env_http_headers: None, }, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: false, required: false, supports_parallel_tool_calls: false, disabled_reason: None, startup_timeout_sec: Some(std::time::Duration::from_secs(5)), tool_timeout_sec: None, default_tools_approval_mode: None, enabled_tools: None, disabled_tools: Some(vec!["forbidden".to_string()]), scopes: None, oauth: Some(McpServerOAuthConfig { client_id: Some("eci-prd-pub-codex-123".to_string()), }), oauth_resource: Some("https://resource.example.com".to_string()), tools: HashMap::new(), }, ); apply_blocking( codex_home, &[ConfigEdit::ReplaceMcpServers(servers.clone())], ) .expect("persist"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = "\[mcp_servers.http]url = \"https://example.com\"bearer_token_env_var = \"TOKEN\"enabled = falsestartup_timeout_sec = 5.0disabled_tools = [\"forbidden\"]oauth_resource = \"https://resource.example.com\"[mcp_servers.http.http_headers]Z-Header = \"z\"[mcp_servers.http.oauth]client_id = \"eci-prd-pub-codex-123\"[mcp_servers.stdio]command = \"cmd\"args = [\"--flag\"]env_vars = [\"FOO\"]supports_parallel_tool_calls = trueenabled_tools = [\"one\", \"two\"][mcp_servers.stdio.env]A = \"1\"B = \"2\""; assert_eq!(raw, expected);}#[test]fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); let mut servers = BTreeMap::new(); servers.insert( "docs".to_string(), McpServerConfig { transport: McpServerTransportConfig::Stdio { command: "docs-server".to_string(), args: Vec::new(), env: None, env_vars: Vec::new(), cwd: None, }, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: true, required: false, supports_parallel_tool_calls: false, disabled_reason: None, startup_timeout_sec: None, tool_timeout_sec: None, default_tools_approval_mode: Some(AppToolApproval::Prompt), enabled_tools: None, disabled_tools: None, scopes: None, oauth: None, oauth_resource: None, tools: HashMap::from([( "search".to_string(), McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), }, )]), }, ); apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = "\[mcp_servers.docs]command = \"docs-server\"default_tools_approval_mode = \"prompt\"[mcp_servers.docs.tools.search]approval_mode = \"approve\""; assert_eq!(raw, expected);}#[test]fn blocking_replace_mcp_servers_preserves_inline_comments() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[mcp_servers]# keep mefoo = { command = "cmd" }"#, ) .expect("seed"); let mut servers = BTreeMap::new(); servers.insert( "foo".to_string(), McpServerConfig { transport: McpServerTransportConfig::Stdio { command: "cmd".to_string(), args: Vec::new(), env: None, env_vars: Vec::new(), cwd: None, }, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: true, required: false, supports_parallel_tool_calls: false, disabled_reason: None, startup_timeout_sec: None, tool_timeout_sec: None, default_tools_approval_mode: None, enabled_tools: None, disabled_tools: None, scopes: None, oauth: None, oauth_resource: None, tools: HashMap::new(), }, ); apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[mcp_servers]# keep mefoo = { command = "cmd" }"#; assert_eq!(contents, expected);}#[test]fn blocking_replace_mcp_servers_preserves_inline_comment_suffix() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[mcp_servers]foo = { command = "cmd" } # keep me"#, ) .expect("seed"); let mut servers = BTreeMap::new(); servers.insert( "foo".to_string(), McpServerConfig { transport: McpServerTransportConfig::Stdio { command: "cmd".to_string(), args: Vec::new(), env: None, env_vars: Vec::new(), cwd: None, }, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: false, required: false, supports_parallel_tool_calls: false, disabled_reason: None, startup_timeout_sec: None, tool_timeout_sec: None, default_tools_approval_mode: None, enabled_tools: None, disabled_tools: None, scopes: None, oauth: None, oauth_resource: None, tools: HashMap::new(), }, ); apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[mcp_servers]foo = { command = "cmd" , enabled = false } # keep me"#; assert_eq!(contents, expected);}#[test]fn blocking_replace_mcp_servers_preserves_inline_comment_after_removing_keys() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[mcp_servers]foo = { command = "cmd", args = ["--flag"] } # keep me"#, ) .expect("seed"); let mut servers = BTreeMap::new(); servers.insert( "foo".to_string(), McpServerConfig { transport: McpServerTransportConfig::Stdio { command: "cmd".to_string(), args: Vec::new(), env: None, env_vars: Vec::new(), cwd: None, }, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: true, required: false, supports_parallel_tool_calls: false, disabled_reason: None, startup_timeout_sec: None, tool_timeout_sec: None, default_tools_approval_mode: None, enabled_tools: None, disabled_tools: None, scopes: None, oauth: None, oauth_resource: None, tools: HashMap::new(), }, ); apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[mcp_servers]foo = { command = "cmd"} # keep me"#; assert_eq!(contents, expected);}#[test]fn blocking_replace_mcp_servers_preserves_inline_comment_prefix_on_update() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), r#"[mcp_servers]# keep mefoo = { command = "cmd" }"#, ) .expect("seed"); let mut servers = BTreeMap::new(); servers.insert( "foo".to_string(), McpServerConfig { transport: McpServerTransportConfig::Stdio { command: "cmd".to_string(), args: Vec::new(), env: None, env_vars: Vec::new(), cwd: None, }, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: false, required: false, supports_parallel_tool_calls: false, disabled_reason: None, startup_timeout_sec: None, tool_timeout_sec: None, default_tools_approval_mode: None, enabled_tools: None, disabled_tools: None, scopes: None, oauth: None, oauth_resource: None, tools: HashMap::new(), }, ); apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"[mcp_servers]# keep mefoo = { command = "cmd" , enabled = false }"#; assert_eq!(contents, expected);}#[test]fn blocking_clear_path_noop_when_missing() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); apply_blocking( codex_home, &[ConfigEdit::ClearPath { segments: vec!["missing".to_string()], }], ) .expect("apply"); assert!( !codex_home.join(CONFIG_TOML_FILE).exists(), "config.toml should not be created on noop" );}#[test]fn blocking_set_path_updates_notifications() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); let item = value(false); apply_blocking( codex_home, &[ConfigEdit::SetPath { segments: vec!["tui".to_string(), "notifications".to_string()], value: item, }], ) .expect("apply"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let config: TomlValue = toml::from_str(&raw).expect("parse config"); let notifications = config .get("tui") .and_then(|item| item.as_table()) .and_then(|tbl| tbl.get("notifications")) .and_then(toml::Value::as_bool); assert_eq!(notifications, Some(false));}#[tokio::test]async fn async_builder_set_model_persists() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path().to_path_buf(); ConfigEditsBuilder::new(&codex_home) .set_model(Some("gpt-5.4"), Some(ReasoningEffort::High)) .apply() .await .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"model = "gpt-5.4"model_reasoning_effort = "high""#; assert_eq!(contents, expected);}#[test]fn blocking_builder_set_model_round_trips_back_and_forth() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); let initial_expected = r#"model = "o4-mini"model_reasoning_effort = "low""#; ConfigEditsBuilder::new(codex_home) .set_model(Some("o4-mini"), Some(ReasoningEffort::Low)) .apply_blocking() .expect("persist initial"); let mut contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents, initial_expected); let updated_expected = r#"model = "gpt-5.4"model_reasoning_effort = "high""#; ConfigEditsBuilder::new(codex_home) .set_model(Some("gpt-5.4"), Some(ReasoningEffort::High)) .apply_blocking() .expect("persist update"); contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents, updated_expected); ConfigEditsBuilder::new(codex_home) .set_model(Some("o4-mini"), Some(ReasoningEffort::Low)) .apply_blocking() .expect("persist revert"); contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents, initial_expected);}#[tokio::test]async fn blocking_set_asynchronous_helpers_available() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path().to_path_buf(); ConfigEditsBuilder::new(&codex_home) .set_hide_full_access_warning(/*acknowledged*/ true) .apply() .await .expect("persist"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let notice = toml::from_str::<TomlValue>(&raw) .expect("parse config") .get("notice") .and_then(|item| item.as_table()) .and_then(|tbl| tbl.get("hide_full_access_warning")) .and_then(toml::Value::as_bool); assert_eq!(notice, Some(true));}#[test]fn blocking_builder_set_realtime_audio_persists_and_clears() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .set_realtime_microphone(Some("USB Mic")) .set_realtime_speaker(Some("Desk Speakers")) .apply_blocking() .expect("persist realtime audio"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let config: TomlValue = toml::from_str(&raw).expect("parse config"); let realtime_audio = config .get("audio") .and_then(TomlValue::as_table) .expect("audio table should exist"); assert_eq!( realtime_audio.get("microphone").and_then(TomlValue::as_str), Some("USB Mic") ); assert_eq!( realtime_audio.get("speaker").and_then(TomlValue::as_str), Some("Desk Speakers") ); ConfigEditsBuilder::new(codex_home) .set_realtime_microphone(/*microphone*/ None) .apply_blocking() .expect("clear realtime microphone"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let config: TomlValue = toml::from_str(&raw).expect("parse config"); let realtime_audio = config .get("audio") .and_then(TomlValue::as_table) .expect("audio table should exist"); assert_eq!(realtime_audio.get("microphone"), None); assert_eq!( realtime_audio.get("speaker").and_then(TomlValue::as_str), Some("Desk Speakers") );}#[test]fn blocking_builder_set_realtime_voice_persists_and_clears() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); ConfigEditsBuilder::new(codex_home) .set_realtime_voice(Some("cedar")) .apply_blocking() .expect("persist realtime voice"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let config: TomlValue = toml::from_str(&raw).expect("parse config"); let realtime = config .get("realtime") .and_then(TomlValue::as_table) .expect("realtime table should exist"); assert_eq!( realtime.get("voice").and_then(TomlValue::as_str), Some("cedar") ); ConfigEditsBuilder::new(codex_home) .set_realtime_voice(/*voice*/ None) .apply_blocking() .expect("clear realtime voice"); let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); let config: TomlValue = toml::from_str(&raw).expect("parse config"); let realtime = config .get("realtime") .and_then(TomlValue::as_table) .expect("realtime table should exist"); assert_eq!(realtime.get("voice"), None);}#[test]fn replace_mcp_servers_blocking_clears_table_when_empty() { let tmp = tempdir().expect("tmpdir"); let codex_home = tmp.path(); std::fs::write( codex_home.join(CONFIG_TOML_FILE), "[mcp_servers]\nfoo = { command = \"cmd\" }\n", ) .expect("seed"); apply_blocking( codex_home, &[ConfigEdit::ReplaceMcpServers(BTreeMap::new())], ) .expect("persist"); let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); assert!(!contents.contains("mcp_servers"));}