use super::*; use anyhow::Result; use axum::http::HeaderValue; use codex_app_server_protocol::AppConfig; use codex_app_server_protocol::AppToolApproval; use codex_app_server_protocol::AppsConfig; use codex_app_server_protocol::AskForApproval; use codex_app_server_protocol::ConfigLayerSource as ApiConfigLayerSource; use codex_config::CloudConfigBundleLoader; use codex_config::LoaderOverrides; use codex_config::test_support::CloudConfigBundleFixture; use codex_http_client::HttpClientFactory; use codex_http_client::OutboundProxyPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::tempdir; #[test] fn toml_value_to_item_handles_nested_config_tables() { let config = r#" [mcp_servers.docs] command = "docs-server" [mcp_servers.docs.http_headers] X-Doc = "42" "#; let value: TomlValue = toml::from_str(config).expect("parse config example"); let item = toml_value_to_item(&value).expect("convert to toml_edit item"); let root = item.as_table().expect("root table"); assert!(!root.is_implicit(), "root table should be explicit"); let mcp_servers = root .get("mcp_servers") .and_then(TomlItem::as_table) .expect("mcp_servers table"); assert!( !mcp_servers.is_implicit(), "mcp_servers table should be explicit" ); let docs = mcp_servers .get("docs") .and_then(TomlItem::as_table) .expect("docs table"); assert_eq!( docs.get("command") .and_then(TomlItem::as_value) .and_then(toml_edit::Value::as_str), Some("docs-server") ); let http_headers = docs .get("http_headers") .and_then(TomlItem::as_table) .expect("http_headers table"); assert_eq!( http_headers .get("X-Doc") .and_then(TomlItem::as_value) .and_then(toml_edit::Value::as_str), Some("42") ); } #[tokio::test] async fn write_value_preserves_comments_and_order() -> Result<()> { let tmp = tempdir().expect("tempdir"); let original = r#"# Codex user configuration model = "gpt-5.2" approval_policy = "on-request" [notice] # Preserve this comment hide_full_access_warning = true [features] unified_exec = true "#; std::fs::write(tmp.path().join(CONFIG_TOML_FILE), original)?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "features.personality".to_string(), value: serde_json::json!(true), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("write succeeds"); let updated = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"); let expected = r#"# Codex user configuration model = "gpt-5.2" approval_policy = "on-request" [notice] # Preserve this comment hide_full_access_warning = true [features] unified_exec = true personality = true "#; assert_eq!(updated, expected); Ok(()) } #[tokio::test] async fn psp_feature_configures_first_party_routing() -> Result<()> { let tmp = tempdir()?; let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), Vec::new(), LoaderOverrides::without_managed_config_for_tests(), CloudConfigBundleLoader::default(), ); let config = service .load_with_overrides( Some( [( "features".to_string(), serde_json::json!({ "apps": true, "psp": true }), )] .into_iter() .collect(), ), Default::default(), ) .await?; assert!(config.features.enabled(codex_features::Feature::Psp)); assert_eq!( config.http_client_factory(), HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault) .with_chatgpt_cookies([HeaderValue::from_static("oai-chat-psp=true")]) ); assert_eq!( config .config_layer_stack .effective_config() .get("features") .and_then(|features| features.get("psp")), Some(&toml::Value::Boolean(true)) ); Ok(()) } #[tokio::test] async fn clear_missing_nested_config_is_noop() -> Result<()> { let tmp = tempdir().expect("tempdir"); let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, "")?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let response = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "features.personality".to_string(), value: serde_json::Value::Null, merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("clear missing config succeeds"); assert_eq!(response.status, WriteStatus::Ok); assert_eq!(response.overridden_metadata, None); assert_eq!(std::fs::read_to_string(&path)?, ""); Ok(()) } #[tokio::test] async fn clearing_user_setting_falls_back_to_packaged_default_without_override() -> Result<()> { let tmp = tempdir()?; let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, "hide_agent_reasoning = true\n")?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let response = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "hide_agent_reasoning".to_string(), value: serde_json::Value::Null, merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await?; assert_eq!(response.status, WriteStatus::Ok); assert_eq!(response.overridden_metadata, None); assert_eq!(std::fs::read_to_string(&path)?, ""); Ok(()) } #[tokio::test] async fn clear_user_value_if_matches_clears_matching_value() -> Result<()> { let tmp = tempdir().expect("tempdir"); let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, "model = \"gpt-5.2\"\napproval_policy = \"never\"\n")?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); service .clear_user_value_if_matches("model", serde_json::json!("gpt-5.2")) .await?; assert_eq!( std::fs::read_to_string(&path)?, "approval_policy = \"never\"\n" ); Ok(()) } #[tokio::test] async fn clear_user_value_if_matches_preserves_non_matching_value() -> Result<()> { let tmp = tempdir().expect("tempdir"); let path = tmp.path().join(CONFIG_TOML_FILE); let original = "model = \"gpt-5.2\"\napproval_policy = \"never\"\n"; std::fs::write(&path, original)?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); service .clear_user_value_if_matches("model", serde_json::json!("gpt-5.3")) .await?; assert_eq!(std::fs::read_to_string(&path)?, original); Ok(()) } #[tokio::test] async fn write_value_rejects_legacy_profile_selector() -> Result<()> { let tmp = tempdir().expect("tempdir"); let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, "model = \"gpt-main\"\n")?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let error = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "profile".to_string(), value: serde_json::json!("work"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect_err("legacy profile selector write should fail"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigValidationError) ); assert!( error .to_string() .contains("`profile` is a legacy config selector"), "{error}" ); assert_eq!(std::fs::read_to_string(&path)?, "model = \"gpt-main\"\n"); Ok(()) } #[tokio::test] async fn write_value_rejects_legacy_profile_table() -> Result<()> { let tmp = tempdir().expect("tempdir"); let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, "")?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let error = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "profiles.work.model".to_string(), value: serde_json::json!("gpt-work"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect_err("legacy profile table write should fail"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigValidationError) ); assert!( error .to_string() .contains("`profiles` contains legacy config profile tables"), "{error}" ); assert_eq!(std::fs::read_to_string(&path)?, ""); Ok(()) } #[tokio::test] async fn batch_write_rejects_legacy_profile_selector() -> Result<()> { let tmp = tempdir().expect("tempdir"); let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, "model = \"gpt-main\"\n")?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let error = service .batch_write(ConfigBatchWriteParams { edits: vec![ codex_app_server_protocol::ConfigEdit { key_path: "model".to_string(), value: serde_json::json!("gpt-work"), merge_strategy: MergeStrategy::Replace, }, codex_app_server_protocol::ConfigEdit { key_path: "profile".to_string(), value: serde_json::json!("work"), merge_strategy: MergeStrategy::Replace, }, ], file_path: Some(path.display().to_string()), expected_version: None, reload_user_config: false, }) .await .expect_err("legacy profile selector batch write should fail"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigValidationError) ); assert!( error .to_string() .contains("`profile` is a legacy config selector"), "{error}" ); assert_eq!(std::fs::read_to_string(&path)?, "model = \"gpt-main\"\n"); Ok(()) } #[tokio::test] async fn write_value_supports_nested_app_paths() -> Result<()> { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "apps".to_string(), value: serde_json::json!({ "app1": { "enabled": false, }, }), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("write apps succeeds"); service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "apps.app1.default_tools_approval_mode".to_string(), value: serde_json::json!("prompt"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("write apps.app1.default_tools_approval_mode succeeds"); let read = service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await .expect("config read succeeds"); assert_eq!( read.config.apps, Some(AppsConfig { default: None, apps: std::collections::HashMap::from([( "app1".to_string(), AppConfig { enabled: false, approvals_reviewer: None, destructive_enabled: None, open_world_enabled: None, default_tools_approval_mode: Some(AppToolApproval::Prompt), default_tools_enabled: None, tools: None, }, )]), }) ); Ok(()) } #[tokio::test] async fn write_value_supports_custom_mcp_server_default_tool_approval_mode() -> Result<()> { let tmp = tempdir().expect("tempdir"); std::fs::write( tmp.path().join(CONFIG_TOML_FILE), "[mcp_servers.docs]\ncommand = \"docs-server\"\n", )?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "mcp_servers.docs.default_tools_approval_mode".to_string(), value: serde_json::json!("approve"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("write mcp server default_tools_approval_mode succeeds"); let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE))?; assert!(contents.contains("default_tools_approval_mode = \"approve\"")); let read = service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await .expect("config read succeeds"); assert_eq!( read.config .additional .get("mcp_servers") .and_then(|servers| servers.get("docs")) .and_then(|docs| docs.get("default_tools_approval_mode")), Some(&serde_json::json!("approve")) ); Ok(()) } #[tokio::test] async fn read_includes_origins_and_layers() { let tmp = tempdir().expect("tempdir"); let user_path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&user_path, "model = \"user\"").unwrap(); let user_file = AbsolutePathBuf::try_from(user_path.clone()).expect("user file"); let managed_path = tmp.path().join("managed_config.toml"); std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap(); let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file"); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()), CloudConfigBundleLoader::default(), ); let response = service .read(ConfigReadParams { include_layers: true, cwd: None, }) .await .expect("response"); assert_eq!(response.config.approval_policy, Some(AskForApproval::Never)); assert_eq!( response .origins .get("approval_policy") .expect("origin") .name, ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file.clone() }, ); let layers = response.layers.expect("layers present"); // Local macOS machines can surface an MDM-managed config layer at the // top of the stack; ignore it so this test stays focused on file/user/system ordering. let layers = if matches!( layers.first().map(|layer| &layer.name), Some(ApiConfigLayerSource::LegacyManagedConfigTomlFromMdm) ) { &layers[1..] } else { layers.as_slice() }; assert_eq!(layers.len(), 3, "expected three layers"); assert_eq!( layers.first().unwrap().name, ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file.clone() } ); assert_eq!( layers.get(1).unwrap().name, ApiConfigLayerSource::User { file: user_file.clone(), profile: None, } ); assert!(matches!( layers.get(2).unwrap().name, ApiConfigLayerSource::System { .. } )); } #[cfg(target_os = "macos")] #[tokio::test] async fn write_value_succeeds_when_managed_preferences_expand_home_directory_paths() -> Result<()> { use base64::Engine; let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n")?; let mut loader_overrides = LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml")); loader_overrides.managed_preferences_base64 = Some( base64::prelude::BASE64_STANDARD.encode( r#" sandbox_mode = "workspace-write" [sandbox_workspace_write] writable_roots = ["~/code"] "# .as_bytes(), ), ); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], loader_overrides, CloudConfigBundleLoader::default(), ); let response = service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "model".to_string(), value: serde_json::json!("updated"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("write succeeds"); assert_eq!(response.status, WriteStatus::Ok); assert_eq!( std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"), "model = \"updated\"\n" ); Ok(()) } #[tokio::test] async fn write_value_reports_override() { let tmp = tempdir().expect("tempdir"); std::fs::write( tmp.path().join(CONFIG_TOML_FILE), "approval_policy = \"on-request\"", ) .unwrap(); let managed_path = tmp.path().join("managed_config.toml"); std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap(); let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file"); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()), CloudConfigBundleLoader::default(), ); let result = service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "approval_policy".to_string(), value: serde_json::json!("never"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("result"); let read_after = service .read(ConfigReadParams { include_layers: true, cwd: None, }) .await .expect("read"); assert_eq!( read_after.config.approval_policy, Some(AskForApproval::Never) ); assert_eq!( read_after .origins .get("approval_policy") .expect("origin") .name, ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file.clone() } ); assert_eq!(result.status, WriteStatus::Ok); assert!(result.overridden_metadata.is_none()); } #[tokio::test] async fn version_conflict_rejected() { let tmp = tempdir().expect("tempdir"); let user_path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&user_path, "model = \"user\"").unwrap(); let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let error = service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "model".to_string(), value: serde_json::json!("gpt-5.2"), merge_strategy: MergeStrategy::Replace, expected_version: Some("sha256:bogus".to_string()), }) .await .expect_err("should fail"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigVersionConflict) ); } #[tokio::test] async fn write_value_defaults_to_user_config_path() { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap(); let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); service .write_value(ConfigValueWriteParams { file_path: None, key_path: "model".to_string(), value: serde_json::json!("gpt-new"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("write succeeds"); let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"); assert!( contents.contains("model = \"gpt-new\""), "config.toml should be updated even when file_path is omitted" ); } #[tokio::test] async fn write_value_defaults_to_selected_user_config_path() { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"gpt-main\"").unwrap(); let selected_path = tmp.path().join("work.config.toml"); std::fs::write(&selected_path, "").unwrap(); let mut loader_overrides = LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml")); loader_overrides.user_config_path = Some(AbsolutePathBuf::from_absolute_path(&selected_path).expect("selected config path")); loader_overrides.user_config_profile = Some("work".parse().expect("profile-v2 name")); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], loader_overrides, CloudConfigBundleLoader::default(), ); service .write_value(ConfigValueWriteParams { file_path: None, key_path: "model".to_string(), value: serde_json::json!("gpt-work"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("write succeeds"); assert_eq!( std::fs::read_to_string(&selected_path).expect("read selected config"), "model = \"gpt-work\"\n" ); assert_eq!( std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read main config"), "model = \"gpt-main\"" ); } #[tokio::test] async fn load_default_config_preserves_managed_requirements_and_selected_user_config_path() { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"gpt-main\"").unwrap(); std::fs::write( tmp.path().join("requirements.toml"), "allowed_login_methods = [\"api\"]\nallowed_chatgpt_workspaces = [\"managed-workspace\"]\n", ) .unwrap(); let selected_path = tmp.path().join("work.config.toml"); std::fs::write(&selected_path, "not valid toml").unwrap(); let selected_file = AbsolutePathBuf::from_absolute_path(&selected_path).expect("selected config path"); let mut loader_overrides = LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml")); loader_overrides.user_config_path = Some(selected_file.clone()); loader_overrides.user_config_profile = Some("work".parse().expect("profile-v2 name")); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], loader_overrides, CloudConfigBundleLoader::default(), ); service .load_latest_config(/*fallback_cwd*/ None) .await .expect_err("selected config should fail to load"); let config = service .load_default_config() .await .expect("default config loads after selected config error"); assert_eq!( config.config_layer_stack.get_user_config_file(), Some(&selected_file) ); assert_eq!( config .config_layer_stack .requirements() .managed_auth_policy(), codex_config::ManagedAuthPolicy { allowed_login_methods: Some(vec![codex_protocol::config_types::ForcedLoginMethod::Api]), allowed_chatgpt_workspaces: Some(vec!["managed-workspace".to_string()]), } ); } #[tokio::test] async fn managed_auth_policy_survives_unusable_requirements_file_changes() -> Result<()> { let tmp = tempdir()?; std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?; let requirements_path = tmp.path().join("requirements.toml"); std::fs::write( &requirements_path, "allowed_login_methods = [\"api\"]\nallowed_chatgpt_workspaces = [\"startup\"]\n", )?; let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), Vec::new(), LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml")), CloudConfigBundleLoader::default(), ); let startup = service.load_latest_config(/*fallback_cwd*/ None).await?; let auth_manager = codex_login::AuthManager::shared_from_config( &startup, /*enable_codex_api_key_env*/ false, ) .await?; std::fs::write( &requirements_path, "allowed_login_methods = [\"chatgpt\"]\nallowed_chatgpt_workspaces = []\n", )?; for refreshed in [ service.load_latest_config(/*fallback_cwd*/ None).await?, service.load_latest_config_for_thread(&startup).await?, ] { assert_eq!(refreshed.forced_login_method, None); assert_eq!(refreshed.forced_chatgpt_workspace_id, None); } assert!( auth_manager.is_login_method_allowed(codex_protocol::config_types::ForcedLoginMethod::Api) ); assert!( !auth_manager .is_login_method_allowed(codex_protocol::config_types::ForcedLoginMethod::Chatgpt) ); assert_eq!( auth_manager.effective_chatgpt_workspaces(), Some(vec!["startup".to_string()]) ); Ok(()) } #[tokio::test] async fn invalid_user_value_rejected_even_if_overridden_by_managed() { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"").unwrap(); let managed_path = tmp.path().join("managed_config.toml"); std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap(); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()), CloudConfigBundleLoader::default(), ); let error = service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "approval_policy".to_string(), value: serde_json::json!("bogus"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect_err("should fail validation"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigValidationError) ); let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents.trim(), "model = \"user\""); } #[tokio::test] async fn reserved_builtin_provider_override_rejected() { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n").unwrap(); let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let error = service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "model_providers.openai.name".to_string(), value: serde_json::json!("OpenAI Override"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect_err("should reject reserved provider override"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigValidationError) ); assert!(error.to_string().contains("reserved built-in provider IDs")); assert!(error.to_string().contains("`openai`")); let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"); assert_eq!(contents, "model = \"user\"\n"); } #[tokio::test] async fn write_value_rejects_invalid_guardian_review_threshold() -> Result<()> { let tmp = tempdir()?; let path = tmp.path().join(CONFIG_TOML_FILE); let initial = "[features.guardianv2]\nenabled = true\nreview_threshold = 0.8\n"; std::fs::write(&path, initial)?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let error = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "features.guardianv2.review_threshold".to_string(), value: serde_json::json!(2.0), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect_err("Guardian review thresholds above 1.0 must be rejected"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigValidationError) ); assert_eq!(std::fs::read_to_string(&path)?, initial); Ok(()) } #[tokio::test] async fn write_value_rejects_feature_requirement_conflict() { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap(); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::without_managed_config_for_tests(), CloudConfigBundleFixture::loader_with_enterprise_requirement( r#" [features] personality = true "#, ), ); let error = service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "features.personality".to_string(), value: serde_json::json!(false), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect_err("conflicting feature write should fail"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigValidationError) ); assert!( error .to_string() .contains("invalid value for `features`: `features.personality=false`"), "{error}" ); assert_eq!( std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).unwrap(), "" ); } #[tokio::test] async fn write_value_rejects_exact_managed_requirement() { let tmp = tempdir().expect("tempdir"); let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, "allow_login_shell = true\n").unwrap(); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::without_managed_config_for_tests(), CloudConfigBundleFixture::loader_with_enterprise_requirement("allow_login_shell = false"), ); let error = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "allow_login_shell".to_string(), value: serde_json::json!(true), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect_err("managed exact field should be read-only"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigRequirementReadonly) ); assert!(error.to_string().contains("`allow_login_shell`")); assert_eq!( std::fs::read_to_string(path).unwrap(), "allow_login_shell = true\n" ); } fn toml_path(tmp: &Path, name: &str) -> String { tmp.join(name).to_string_lossy().replace('\\', "\\\\") } #[tokio::test] async fn read_omits_origins_for_exact_managed_values() { for has_user_values in [true, false] { let tmp = tempdir().expect("tempdir"); let user_config = if has_user_values { format!( r#"model = "user-model" sqlite_home = "{}" allow_login_shell = true [feedback] enabled = true "#, toml_path(tmp.path(), "user-sqlite"), ) } else { "model = \"user-model\"\n".to_string() }; std::fs::write(tmp.path().join(CONFIG_TOML_FILE), user_config).unwrap(); let requirements = format!( r#"sqlite_home = "{}" allow_login_shell = false [feedback] enabled = false "#, toml_path(tmp.path(), "managed-sqlite"), ); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::without_managed_config_for_tests(), CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements), ); let response = service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await .expect("config read should succeed"); assert_eq!( response.config.additional.get("sqlite_home"), Some(&serde_json::json!(tmp.path().join("managed-sqlite"))) ); assert_eq!( response.config.additional.get("allow_login_shell"), Some(&serde_json::json!(false)) ); assert_eq!( response.config.additional.get("feedback"), Some(&serde_json::json!({"enabled": false})) ); for path in ["sqlite_home", "allow_login_shell", "feedback.enabled"] { assert!(!response.origins.contains_key(path), "origin for {path}"); } assert!(response.origins.contains_key("model")); } } #[tokio::test] async fn read_materializes_default_allow_login_shell() { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap(); let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let response = service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await .expect("config read should succeed"); assert_eq!( response.config.additional.get("allow_login_shell"), Some(&serde_json::json!(true)) ); } #[tokio::test] async fn write_value_allows_unmanaged_sibling_of_exact_requirement() { let tmp = tempdir().expect("tempdir"); let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, "").unwrap(); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::without_managed_config_for_tests(), CloudConfigBundleFixture::loader_with_enterprise_requirement( r#" [windows] sandbox_private_desktop = false "#, ), ); service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "windows.sandbox".to_string(), value: serde_json::json!("elevated"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("unmanaged sibling should remain writable"); assert!( std::fs::read_to_string(path) .unwrap() .contains("sandbox = \"elevated\"") ); } #[tokio::test] async fn read_reports_managed_overrides_user_and_session_flags() { let tmp = tempdir().expect("tempdir"); let user_path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&user_path, "model = \"user\"").unwrap(); let user_file = AbsolutePathBuf::try_from(user_path.clone()).expect("user file"); let managed_path = tmp.path().join("managed_config.toml"); std::fs::write(&managed_path, "model = \"system\"").unwrap(); let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file"); let cli_overrides = vec![( "model".to_string(), TomlValue::String("session".to_string()), )]; let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), cli_overrides, LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()), CloudConfigBundleLoader::default(), ); let response = service .read(ConfigReadParams { include_layers: true, cwd: None, }) .await .expect("response"); assert_eq!(response.config.model.as_deref(), Some("system")); assert_eq!( response.origins.get("model").expect("origin").name, ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file.clone() }, ); let layers = response.layers.expect("layers"); // Local macOS machines can surface an MDM-managed config layer at the // top of the stack; ignore it so this test stays focused on file/session/user ordering. let layers = if matches!( layers.first().map(|layer| &layer.name), Some(ApiConfigLayerSource::LegacyManagedConfigTomlFromMdm) ) { &layers[1..] } else { layers.as_slice() }; assert_eq!( layers.first().unwrap().name, ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file } ); assert_eq!( layers.get(1).unwrap().name, ApiConfigLayerSource::SessionFlags ); assert_eq!( layers.get(2).unwrap().name, ApiConfigLayerSource::User { file: user_file, profile: None } ); } #[tokio::test] async fn write_value_reports_managed_override() { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap(); let managed_path = tmp.path().join("managed_config.toml"); std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap(); let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file"); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()), CloudConfigBundleLoader::default(), ); let result = service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), key_path: "approval_policy".to_string(), value: serde_json::json!("on-request"), merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("result"); assert_eq!(result.status, WriteStatus::OkOverridden); let overridden = result.overridden_metadata.expect("overridden metadata"); assert_eq!( overridden.overriding_layer.name, ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file } ); assert_eq!(overridden.effective_value, serde_json::json!("never")); } /// Legacy managed feature toggles own their normalized enabled origin and override metadata. #[tokio::test] async fn multi_agent_v2_boolean_layer_owns_enabled_origin_and_overrides() { let tmp = tempdir().expect("tempdir"); let user_path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write( &user_path, "[features.multi_agent_v2]\nenabled = true\nsubagent_usage_hint_text = \"keep\"\n", ) .expect("user config"); let managed_path = tmp.path().join("managed_config.toml"); std::fs::write(&managed_path, "[features]\nmulti_agent_v2 = false\n").expect("managed config"); let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file"); let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::with_managed_config_path_for_tests(managed_path), CloudConfigBundleLoader::default(), ); let read = service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await .expect("read config"); assert_eq!( read.origins .get("features.multi_agent_v2.enabled") .expect("enabled origin") .name, ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file.clone(), }, ); let result = service .write_value(ConfigValueWriteParams { file_path: Some(user_path.display().to_string()), key_path: "features.multi_agent_v2.enabled".to_string(), value: serde_json::json!(true), merge_strategy: MergeStrategy::Upsert, expected_version: None, }) .await .expect("write config"); assert_eq!(result.status, WriteStatus::OkOverridden); let overridden = result.overridden_metadata.expect("overridden metadata"); assert_eq!( overridden.overriding_layer.name, ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file } ); assert_eq!(overridden.effective_value, serde_json::json!(false)); } #[tokio::test] async fn upsert_merges_tables_replace_overwrites() -> Result<()> { let tmp = tempdir().expect("tempdir"); let path = tmp.path().join(CONFIG_TOML_FILE); let base = r#"[mcp_servers.linear] bearer_token_env_var = "TOKEN" name = "linear" url = "https://linear.example" [mcp_servers.linear.env_http_headers] existing = "keep" [mcp_servers.linear.http_headers] alpha = "a" "#; let overlay = serde_json::json!({ "bearer_token_env_var": "NEW_TOKEN", "http_headers": { "alpha": "updated", "beta": "b" }, "name": "linear", "url": "https://linear.example" }); std::fs::write(&path, base)?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "mcp_servers.linear".to_string(), value: overlay.clone(), merge_strategy: MergeStrategy::Upsert, expected_version: None, }) .await .expect("upsert succeeds"); let upserted: TomlValue = toml::from_str(&std::fs::read_to_string(&path)?)?; let expected_upsert: TomlValue = toml::from_str( r#"[mcp_servers.linear] bearer_token_env_var = "NEW_TOKEN" name = "linear" url = "https://linear.example" [mcp_servers.linear.env_http_headers] existing = "keep" [mcp_servers.linear.http_headers] alpha = "updated" beta = "b" "#, )?; assert_eq!(upserted, expected_upsert); std::fs::write(&path, base)?; service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "mcp_servers.linear".to_string(), value: overlay, merge_strategy: MergeStrategy::Replace, expected_version: None, }) .await .expect("replace succeeds"); let replaced: TomlValue = toml::from_str(&std::fs::read_to_string(&path)?)?; let expected_replace: TomlValue = toml::from_str( r#"[mcp_servers.linear] bearer_token_env_var = "NEW_TOKEN" name = "linear" url = "https://linear.example" [mcp_servers.linear.http_headers] alpha = "updated" beta = "b" "#, )?; assert_eq!(replaced, expected_replace); Ok(()) } #[tokio::test] async fn config_writes_apply_path_sensitive_merge_rules() -> Result<()> { let cases = [ ( r#"[shell_environment_policy] exclude = ["AWS_*"] "#, "shell_environment_policy", serde_json::json!({"filters": {"AWS_*": "include"}}), r#"[shell_environment_policy.filters] "AWS_*" = "include" "#, ), ( r#"[shell_environment_policy] inherit = "core" exclude = ["AWS_*"] "#, "shell_environment_policy.filters", serde_json::json!({"AWS_*": "include"}), r#"[shell_environment_policy] inherit = "core" [shell_environment_policy.filters] "AWS_*" = "include" "#, ), ( r#"[shell_environment_policy.filters] "AWS_*" = "include" "#, "shell_environment_policy.exclude", serde_json::json!(["AWS_*"]), r#"[shell_environment_policy] exclude = ["AWS_*"] "#, ), ( r#"[shell_environment_policy] exclude = ["AWS_*"] include_only = ["PATH"] "#, "shell_environment_policy.filters", serde_json::json!({}), r#"[shell_environment_policy.filters] "#, ), ( r#"[shell_environment_policy.filters] "AWS_*" = "include" "#, "shell_environment_policy.exclude", serde_json::json!([]), r#"[shell_environment_policy] exclude = [] "#, ), ( r#"[shell_environment_policy.filters] "aws_*" = "exclude" "#, "shell_environment_policy.filters", serde_json::json!({"AWS_*": "include"}), r#"[shell_environment_policy.filters] "aws_*" = "include" "#, ), ( r#"[shell_environment_policy.filters] "aws_*" = "exclude" "#, "shell_environment_policy.filters.AWS_*", serde_json::json!("include"), r#"[shell_environment_policy.filters] "aws_*" = "include" "#, ), ( r#"[shell_environment_policy.filters] "секрет_*" = "exclude" "#, "shell_environment_policy.filters.СЕКРЕТ_*", serde_json::json!("include"), r#"[shell_environment_policy.filters] "секрет_*" = "include" "#, ), ( r#"[permissions.dev.network.domains] "example.com" = "deny" "#, "permissions.dev.network.domains", serde_json::json!({"EXAMPLE.COM": "allow"}), r#"[permissions.dev.network.domains] "example.com" = "allow" "#, ), ( r#"[memories] no_memories_if_mcp_or_web_search = false "#, "memories", serde_json::json!({"disable_on_external_context": true}), r#"[memories] disable_on_external_context = true "#, ), ( r#"[features] multi_agent_v2 = true "#, "features.multi_agent_v2.subagent_usage_hint_text", serde_json::json!("Delegate carefully."), r#"[features.multi_agent_v2] enabled = true subagent_usage_hint_text = "Delegate carefully." "#, ), ( r#"[features] multi_agent_v2 = true "#, "features.multi_agent_v2", serde_json::json!({"subagent_usage_hint_text": "Delegate carefully."}), r#"[features.multi_agent_v2] enabled = true subagent_usage_hint_text = "Delegate carefully." "#, ), ( r#"[features.multi_agent_v2] enabled = true subagent_usage_hint_text = "Delegate carefully." "#, "features.multi_agent_v2", serde_json::json!(false), r#"[features.multi_agent_v2] enabled = false subagent_usage_hint_text = "Delegate carefully." "#, ), ( r#"[features.multi_agent_v2] enabled = true subagent_usage_hint_text = "Delegate carefully." "#, "features.multi_agent_v2", serde_json::Value::Null, "", ), ( r#"[desktop.features.multi_agent_v2] custom = true "#, "desktop.features.multi_agent_v2", serde_json::json!(false), r#"[desktop.features] multi_agent_v2 = false "#, ), ( r#"[desktop.features] multi_agent_v2 = true "#, "desktop.features.multi_agent_v2", serde_json::json!({"custom": true}), r#"[desktop.features.multi_agent_v2] custom = true "#, ), ]; for (base, key_path, value, expected) in cases { let tmp = tempdir()?; let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, base)?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: key_path.to_string(), value, merge_strategy: MergeStrategy::Upsert, expected_version: None, }) .await?; let updated: TomlValue = toml::from_str(&std::fs::read_to_string(&path)?)?; let expected: TomlValue = toml::from_str(expected)?; assert_eq!(updated, expected); service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await?; } Ok(()) } #[tokio::test] async fn clear_shell_environment_filter_ignores_ascii_case() -> Result<()> { let tmp = tempdir()?; let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write( &path, r#"[shell_environment_policy.filters] "aws_*" = "exclude" "keep_*" = "include" "#, )?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let response = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "shell_environment_policy.filters.AWS_*".to_string(), value: serde_json::Value::Null, merge_strategy: MergeStrategy::Upsert, expected_version: None, }) .await?; assert_eq!(response.status, WriteStatus::Ok); assert_eq!(response.overridden_metadata, None); assert_eq!( std::fs::read_to_string(&path)?, r#"[shell_environment_policy.filters] "keep_*" = "include" "# ); service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await?; Ok(()) } #[tokio::test] async fn upsert_shell_environment_scalar_preserves_unrelated_formatting() -> Result<()> { let tmp = tempdir()?; let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write( &path, r#"[shell_environment_policy] inherit = "all" exclude = [ "AWS_*", # keep this comment ] set = { KEEP = "1", OTHER = "2" } # keep this inline table "#, )?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "shell_environment_policy.inherit".to_string(), value: serde_json::json!("core"), merge_strategy: MergeStrategy::Upsert, expected_version: None, }) .await?; assert_eq!( std::fs::read_to_string(&path)?, r#"[shell_environment_policy] inherit = "core" exclude = [ "AWS_*", # keep this comment ] set = { KEEP = "1", OTHER = "2" } # keep this inline table "# ); service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await?; Ok(()) } #[tokio::test] async fn upsert_shell_environment_filter_scalar_preserves_formatting_and_version() -> Result<()> { let tmp = tempdir()?; let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write( &path, r#"[shell_environment_policy] set = { KEEP = "1", OTHER = "2" } # keep this inline table [shell_environment_policy.filters] "AWS_*" = "exclude" # keep this edited comment "KEEP_*" = "include" # keep this untouched comment "#, )?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let response = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "shell_environment_policy.filters.aws_*".to_string(), value: serde_json::json!("include"), merge_strategy: MergeStrategy::Upsert, expected_version: None, }) .await?; assert_eq!( std::fs::read_to_string(&path)?, r#"[shell_environment_policy] set = { KEEP = "1", OTHER = "2" } # keep this inline table [shell_environment_policy.filters] "AWS_*" = "include" # keep this edited comment "KEEP_*" = "include" # keep this untouched comment "# ); service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "shell_environment_policy.filters.AWS_*".to_string(), value: serde_json::json!("exclude"), merge_strategy: MergeStrategy::Upsert, expected_version: Some(response.version), }) .await?; service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await?; Ok(()) } #[tokio::test] async fn shell_environment_upsert_rejects_case_variant_filters_in_one_edit() -> Result<()> { let tmp = tempdir()?; let path = tmp.path().join(CONFIG_TOML_FILE); let initial = r#"[shell_environment_policy.filters] "KEEP_*" = "include" "#; std::fs::write(&path, initial)?; let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); let error = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: "shell_environment_policy.filters".to_string(), value: serde_json::json!({"AWS_*": "include", "aws_*": "exclude"}), merge_strategy: MergeStrategy::Upsert, expected_version: None, }) .await .expect_err("one filter-map edit must not contain case-variant keys"); assert_eq!( error.write_error_code(), Some(ConfigWriteErrorCode::ConfigValidationError) ); assert!( error .to_string() .contains("duplicate shell environment filter") ); assert_eq!(std::fs::read_to_string(&path)?, initial); Ok(()) } #[tokio::test] async fn shell_environment_representation_switch_reports_managed_override() -> Result<()> { let cases = [ ( r#"[shell_environment_policy] exclude = ["AWS_*"] "#, "shell_environment_policy.filters.AWS_*", serde_json::json!("include"), ), ( r#"[shell_environment_policy.filters] "AWS_*" = "include" "#, "shell_environment_policy.exclude", serde_json::json!(["AWS_*"]), ), ]; for (managed, key_path, value) in cases { let tmp = tempdir()?; let path = tmp.path().join(CONFIG_TOML_FILE); std::fs::write(&path, "")?; let managed_path = tmp.path().join("managed_config.toml"); std::fs::write(&managed_path, managed)?; let managed_file = AbsolutePathBuf::try_from(managed_path.clone())?; let service = ConfigManager::new_for_tests( tmp.path().to_path_buf(), vec![], LoaderOverrides::with_managed_config_path_for_tests(managed_path), CloudConfigBundleLoader::default(), ); let response = service .write_value(ConfigValueWriteParams { file_path: Some(path.display().to_string()), key_path: key_path.to_string(), value, merge_strategy: MergeStrategy::Upsert, expected_version: None, }) .await?; assert_eq!(response.status, WriteStatus::OkOverridden); let overridden = response .overridden_metadata .expect("managed representation should override the user edit"); assert_eq!( overridden.overriding_layer.name, ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file } ); assert_eq!(overridden.effective_value, serde_json::Value::Null); service .read(ConfigReadParams { include_layers: false, cwd: None, }) .await?; } Ok(()) }