Download codex-rs/core/src/config/edit_tests.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 51.9 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/src/config/edit_tests.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core/src/config/edit_tests.rs
-
curl -L -o edit_tests.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/src/config/edit_tests.rs
51.9 kB
| 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; | |
| use std::os::unix::fs::symlink; | |
| use tempfile::tempdir; | |
| use toml::Value as TomlValue; | |
| 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); | |
| } | |
| 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"); | |
| } | |
| 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"); | |
| } | |
| 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"); | |
| } | |
| 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"); | |
| } | |
| /// Toggling multi-agent v2 must preserve settings stored in its feature table. | |
| fn multi_agent_v2_feature_toggle_preserves_nested_configuration() { | |
| let tmp = tempdir().expect("tmpdir"); | |
| let codex_home = tmp.path(); | |
| let config_path = codex_home.join(CONFIG_TOML_FILE); | |
| std::fs::write( | |
| &config_path, | |
| "[features.multi_agent_v2]\nenabled = true\nsubagent_usage_hint_text = \"Delegate carefully.\"\n", | |
| ) | |
| .expect("write config"); | |
| ConfigEditsBuilder::new(codex_home) | |
| .set_feature_enabled("multi_agent_v2", /*enabled*/ false) | |
| .apply_blocking() | |
| .expect("disable feature"); | |
| let disabled: TomlValue = | |
| toml::from_str(&std::fs::read_to_string(&config_path).expect("read disabled config")) | |
| .expect("parse disabled config"); | |
| assert_eq!( | |
| disabled, | |
| toml::from_str::<TomlValue>( | |
| "[features.multi_agent_v2]\nenabled = false\nsubagent_usage_hint_text = \"Delegate carefully.\"\n", | |
| ) | |
| .expect("parse expected config") | |
| ); | |
| ConfigEditsBuilder::new(codex_home) | |
| .set_feature_enabled("multi_agent_v2", /*enabled*/ true) | |
| .apply_blocking() | |
| .expect("enable feature"); | |
| let enabled: TomlValue = | |
| toml::from_str(&std::fs::read_to_string(&config_path).expect("read enabled config")) | |
| .expect("parse enabled config"); | |
| assert_eq!( | |
| enabled, | |
| toml::from_str::<TomlValue>( | |
| "[features.multi_agent_v2]\nenabled = true\nsubagent_usage_hint_text = \"Delegate carefully.\"\n", | |
| ) | |
| .expect("parse expected config") | |
| ); | |
| } | |
| fn sleep_tool_feature_toggle_preserves_mode() { | |
| let tmp = tempdir().expect("tmpdir"); | |
| let codex_home = tmp.path(); | |
| let config_path = codex_home.join(CONFIG_TOML_FILE); | |
| std::fs::write( | |
| &config_path, | |
| "[features.sleep_tool]\nmode = \"always_on\"\n", | |
| ) | |
| .expect("write config"); | |
| for enabled in [false, true] { | |
| ConfigEditsBuilder::new(codex_home) | |
| .set_feature_enabled("sleep_tool", enabled) | |
| .apply_blocking() | |
| .expect("toggle feature"); | |
| let actual: TomlValue = | |
| toml::from_str(&std::fs::read_to_string(&config_path).expect("read config")) | |
| .expect("parse config"); | |
| let expected: TomlValue = toml::from_str(&format!( | |
| "[features.sleep_tool]\nenabled = {enabled}\nmode = \"always_on\"\n", | |
| )) | |
| .expect("parse expected config"); | |
| assert_eq!(actual, expected); | |
| } | |
| } | |
| fn network_proxy_feature_toggle_preserves_credential_broker_configuration() { | |
| let tmp = tempdir().expect("tmpdir"); | |
| let codex_home = tmp.path(); | |
| let config_path = codex_home.join(CONFIG_TOML_FILE); | |
| for (initial, disabled, enabled_again) in [ | |
| ( | |
| "[features]\nnetwork_proxy = true\n", | |
| "[features]\n", | |
| "[features]\nnetwork_proxy = true\n", | |
| ), | |
| ( | |
| "[features.network_proxy]\nenabled = true\ncredential_broker = true\ndomains = { \"github.com\" = \"allow\" }\n", | |
| "[features.network_proxy]\nenabled = false\ncredential_broker = true\ndomains = { \"github.com\" = \"allow\" }\n", | |
| "[features.network_proxy]\nenabled = true\ncredential_broker = true\ndomains = { \"github.com\" = \"allow\" }\n", | |
| ), | |
| ( | |
| "[features.network_proxy]\nenabled = true\ncredentials = { vendor = { env = [\"VENDOR_TOKEN\"] } }\n", | |
| "[features.network_proxy]\nenabled = false\ncredentials = { vendor = { env = [\"VENDOR_TOKEN\"] } }\n", | |
| "[features.network_proxy]\nenabled = true\ncredentials = { vendor = { env = [\"VENDOR_TOKEN\"] } }\n", | |
| ), | |
| ] { | |
| std::fs::write(&config_path, initial).expect("write config"); | |
| for (enabled, expected) in [(false, disabled), (true, enabled_again)] { | |
| ConfigEditsBuilder::new(codex_home) | |
| .set_feature_enabled("network_proxy", enabled) | |
| .apply_blocking() | |
| .expect("toggle feature"); | |
| let updated: TomlValue = | |
| toml::from_str(&std::fs::read_to_string(&config_path).expect("read config")) | |
| .expect("parse config"); | |
| assert_eq!(updated, toml::from_str::<TomlValue>(expected).unwrap()); | |
| } | |
| } | |
| } | |
| /// Adding nested multi-agent settings must retain an existing legacy boolean toggle. | |
| fn multi_agent_v2_nested_edit_preserves_legacy_boolean_toggle() { | |
| for feature_path in ["features", "profiles.work.features"] { | |
| let tmp = tempdir().expect("tmpdir"); | |
| let codex_home = tmp.path(); | |
| let config_path = codex_home.join(CONFIG_TOML_FILE); | |
| std::fs::write( | |
| &config_path, | |
| format!("[{feature_path}]\nmulti_agent_v2 = true\n"), | |
| ) | |
| .expect("write config"); | |
| let mut feature_segments = feature_path | |
| .split('.') | |
| .map(str::to_string) | |
| .collect::<Vec<_>>(); | |
| feature_segments.push("multi_agent_v2".to_string()); | |
| let mut instruction_segments = feature_segments.clone(); | |
| instruction_segments.push("subagent_usage_hint_text".to_string()); | |
| ConfigEditsBuilder::new(codex_home) | |
| .with_edits([ConfigEdit::SetPath { | |
| segments: instruction_segments, | |
| value: value("Delegate carefully."), | |
| }]) | |
| .apply_blocking() | |
| .expect("persist nested config"); | |
| let updated: TomlValue = | |
| toml::from_str(&std::fs::read_to_string(&config_path).expect("read config")) | |
| .expect("parse config"); | |
| assert_eq!( | |
| updated, | |
| toml::from_str::<TomlValue>(&format!( | |
| "[{feature_path}.multi_agent_v2]\nenabled = true\nsubagent_usage_hint_text = \"Delegate carefully.\"\n", | |
| )) | |
| .expect("parse expected config") | |
| ); | |
| ConfigEditsBuilder::new(codex_home) | |
| .with_edits([ConfigEdit::SetPath { | |
| segments: feature_segments.clone(), | |
| value: value(false), | |
| }]) | |
| .apply_blocking() | |
| .expect("disable feature"); | |
| let disabled: TomlValue = | |
| toml::from_str(&std::fs::read_to_string(&config_path).expect("read config")) | |
| .expect("parse config"); | |
| assert_eq!( | |
| disabled, | |
| toml::from_str::<TomlValue>(&format!( | |
| "[{feature_path}.multi_agent_v2]\nenabled = false\nsubagent_usage_hint_text = \"Delegate carefully.\"\n", | |
| )) | |
| .expect("parse expected config") | |
| ); | |
| ConfigEditsBuilder::new(codex_home) | |
| .with_edits([ConfigEdit::ClearPath { | |
| segments: feature_segments, | |
| }]) | |
| .apply_blocking() | |
| .expect("clear feature toggle"); | |
| let cleared: TomlValue = | |
| toml::from_str(&std::fs::read_to_string(&config_path).expect("read config")) | |
| .expect("parse config"); | |
| assert_eq!( | |
| feature_path | |
| .split('.') | |
| .try_fold(&cleared, |config, segment| config.get(segment)) | |
| .and_then(|features| features.get("multi_agent_v2")), | |
| None | |
| ); | |
| } | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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"]) | |
| ); | |
| } | |
| 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") | |
| ); | |
| } | |
| 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") | |
| ); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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, ""); | |
| } | |
| 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); | |
| } | |
| 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") | |
| ); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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" | |
| # ok | |
| url = "https://linear.example" | |
| [mcp_servers.linear.http_headers] | |
| foo = "bar" | |
| [sandbox_workspace_write] | |
| # ok 3 | |
| network_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" | |
| # ok | |
| url = "https://linear.example/v2" | |
| [mcp_servers.linear.http_headers] | |
| foo = "bar" | |
| [sandbox_workspace_write] | |
| # ok 3 | |
| network_access = true | |
| "#; | |
| assert_eq!(updated, expected); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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 me | |
| existing = "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 me | |
| existing = "value" | |
| hide_full_access_warning = true | |
| "#; | |
| assert_eq!(contents, expected); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| fn blocking_replace_mcp_servers_round_trips() { | |
| let tmp = tempdir().expect("tmpdir"); | |
| let codex_home = tmp.path(); | |
| let mut servers = BTreeMap::new(); | |
| servers.insert("ema".to_string(), serde_json::from_value(serde_json::json!({ | |
| "url": "https://ema.example/mcp", "auth": "ema_auth", "scopes": ["tools"], | |
| "oauth_resource": "https://ema.example", | |
| "oauth": {"client_id": "resource-client", "authorization_server_issuer": "https://as.example"} | |
| })).expect("EMA server")); | |
| servers.insert( | |
| "stdio".to_string(), | |
| McpServerConfig { | |
| auth: Default::default(), | |
| 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, | |
| omit_tools_from: None, | |
| 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 { | |
| auth: Default::default(), | |
| 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, | |
| http_headers_helper: Some("auth-cli headers".to_string()), | |
| }, | |
| environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), | |
| enabled: false, | |
| required: false, | |
| supports_parallel_tool_calls: false, | |
| omit_tools_from: None, | |
| 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()), | |
| callback_url: Some("http://127.0.0.1/callback/example".to_string()), | |
| callback_port: Some(9876), | |
| ..Default::default() | |
| }), | |
| 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.ema] | |
| url = \"https://ema.example/mcp\" | |
| auth = \"ema_auth\" | |
| scopes = [\"tools\"] | |
| oauth_resource = \"https://ema.example\" | |
| [mcp_servers.ema.oauth] | |
| client_id = \"resource-client\" | |
| authorization_server_issuer = \"https://as.example\" | |
| [mcp_servers.http] | |
| url = \"https://example.com\" | |
| bearer_token_env_var = \"TOKEN\" | |
| http_headers_helper = \"auth-cli headers\" | |
| enabled = false | |
| startup_timeout_sec = 5.0 | |
| disabled_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\" | |
| callback_url = \"http://127.0.0.1/callback/example\" | |
| callback_port = 9876 | |
| [mcp_servers.stdio] | |
| command = \"cmd\" | |
| args = [\"--flag\"] | |
| env_vars = [\"FOO\"] | |
| supports_parallel_tool_calls = true | |
| enabled_tools = [\"one\", \"two\"] | |
| [mcp_servers.stdio.env] | |
| A = \"1\" | |
| B = \"2\" | |
| "; | |
| assert_eq!(raw, expected); | |
| } | |
| fn blocking_replace_mcp_servers_serializes_tool_approval_overrides(output_token_limit: usize) { | |
| let tmp = tempdir().expect("tmpdir"); | |
| let codex_home = tmp.path(); | |
| let mut servers = BTreeMap::new(); | |
| servers.insert( | |
| "docs".to_string(), | |
| McpServerConfig { | |
| auth: Default::default(), | |
| 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, | |
| omit_tools_from: None, | |
| 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), | |
| output_token_limit: std::num::NonZeroUsize::new(output_token_limit), | |
| }, | |
| )]), | |
| }, | |
| ); | |
| let result = apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]); | |
| if i64::try_from(output_token_limit).is_err() { | |
| assert!( | |
| result | |
| .unwrap_err() | |
| .to_string() | |
| .contains("output_token_limit exceeds the TOML integer range") | |
| ); | |
| return; | |
| } | |
| result.expect("persist"); | |
| let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); | |
| let expected = format!( | |
| "\ | |
| [mcp_servers.docs] | |
| command = \"docs-server\" | |
| default_tools_approval_mode = \"prompt\" | |
| [mcp_servers.docs.tools.search] | |
| approval_mode = \"approve\" | |
| output_token_limit = {output_token_limit} | |
| " | |
| ); | |
| assert_eq!(raw, expected); | |
| } | |
| 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 me | |
| foo = { command = "cmd" } | |
| "#, | |
| ) | |
| .expect("seed"); | |
| let mut servers = BTreeMap::new(); | |
| servers.insert( | |
| "foo".to_string(), | |
| McpServerConfig { | |
| auth: Default::default(), | |
| 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, | |
| omit_tools_from: None, | |
| 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 me | |
| foo = { command = "cmd" } | |
| "#; | |
| assert_eq!(contents, expected); | |
| } | |
| 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 { | |
| auth: Default::default(), | |
| 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, | |
| omit_tools_from: None, | |
| 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); | |
| } | |
| 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 { | |
| auth: Default::default(), | |
| 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, | |
| omit_tools_from: None, | |
| 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); | |
| } | |
| 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 me | |
| foo = { command = "cmd" } | |
| "#, | |
| ) | |
| .expect("seed"); | |
| let mut servers = BTreeMap::new(); | |
| servers.insert( | |
| "foo".to_string(), | |
| McpServerConfig { | |
| auth: Default::default(), | |
| 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, | |
| omit_tools_from: None, | |
| 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 me | |
| foo = { command = "cmd" , enabled = false } | |
| "#; | |
| assert_eq!(contents, expected); | |
| } | |
| 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" | |
| ); | |
| } | |
| 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)); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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)); | |
| } | |
| 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") | |
| ); | |
| } | |
| 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); | |
| } | |
| 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")); | |
| } | |