Codex Handbook
core/src/config/edit_tests.rs 1441 lines
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"));}