From ec03f06cb6bce38aa11aad4c9b00701c3fb92002 Mon Sep 17 00:00:00 2001 From: Fini Date: Mon, 1 Jun 2026 03:18:15 +0800 Subject: [PATCH] fix(settings): infer ark coding preset by model --- crates/op-editor-core/src/agent_settings.rs | 2 +- .../src/agent_settings_builtin_presets.rs | 71 ++++- crates/op-editor-core/src/lib.rs | 4 +- .../src/tests_agent_settings.rs | 20 ++ crates/op-host-desktop/src/settings_io.rs | 274 +---------------- .../op-host-desktop/src/settings_io_tests.rs | 286 ++++++++++++++++++ 6 files changed, 380 insertions(+), 277 deletions(-) create mode 100644 crates/op-host-desktop/src/settings_io_tests.rs diff --git a/crates/op-editor-core/src/agent_settings.rs b/crates/op-editor-core/src/agent_settings.rs index 0ca2ec5f2..b7d6dbdca 100644 --- a/crates/op-editor-core/src/agent_settings.rs +++ b/crates/op-editor-core/src/agent_settings.rs @@ -609,7 +609,7 @@ impl AgentSettings { self.next_builtin_agent_id = self.next_builtin_agent_id.max(1).saturating_add(1); self.builtin_agents.push(BuiltinAgentConfig { id: id.clone(), - preset: infer_builtin_agent_preset(kind, &base_url), + preset: infer_builtin_agent_preset(kind, &base_url, &model), display_name, kind, api_key, diff --git a/crates/op-editor-core/src/agent_settings_builtin_presets.rs b/crates/op-editor-core/src/agent_settings_builtin_presets.rs index 02a539cb7..bdb98123b 100644 --- a/crates/op-editor-core/src/agent_settings_builtin_presets.rs +++ b/crates/op-editor-core/src/agent_settings_builtin_presets.rs @@ -282,19 +282,64 @@ pub fn builtin_agent_preset(key: BuiltinAgentPresetKey) -> BuiltinAgentPreset { .unwrap_or(BUILTIN_AGENT_PRESETS[0]) } -pub fn infer_builtin_agent_preset(kind: BuiltinAgentKind, base_url: &str) -> BuiltinAgentPresetKey { +pub fn infer_builtin_agent_preset( + kind: BuiltinAgentKind, + base_url: &str, + model: &str, +) -> BuiltinAgentPresetKey { let normalized = base_url.trim().trim_end_matches('/'); - BUILTIN_AGENT_PRESETS + let model = model.trim(); + let mut matches = BUILTIN_AGENT_PRESETS .iter() - .find(|preset| { - preset.base_url.trim_end_matches('/') == normalized - || preset - .alt_base_url - .is_some_and(|url| url.trim_end_matches('/') == normalized) - }) - .map(|preset| preset.key) - .unwrap_or(match kind { - BuiltinAgentKind::Anthropic => BuiltinAgentPresetKey::Anthropic, - BuiltinAgentKind::OpenAiCompat => BuiltinAgentPresetKey::Custom, - }) + .filter(|preset| preset_url_matches(**preset, normalized)); + + if let Some(preset) = matches + .clone() + .find(|preset| preset_kind_matches(**preset, kind) && preset.model == model) + { + return preset.key; + } + if let Some(preset) = matches.find(|preset| preset_kind_matches(**preset, kind)) { + return preset.key; + } + match kind { + BuiltinAgentKind::Anthropic => BuiltinAgentPresetKey::Anthropic, + BuiltinAgentKind::OpenAiCompat => BuiltinAgentPresetKey::Custom, + } +} + +pub fn normalize_builtin_agent_preset( + saved: BuiltinAgentPresetKey, + kind: BuiltinAgentKind, + base_url: &str, + model: &str, +) -> BuiltinAgentPresetKey { + let inferred = infer_builtin_agent_preset(kind, base_url, model); + if inferred == saved { + return saved; + } + let saved_preset = builtin_agent_preset(saved); + let inferred_preset = builtin_agent_preset(inferred); + let normalized = base_url.trim().trim_end_matches('/'); + let model = model.trim(); + if preset_url_matches(saved_preset, normalized) + && preset_kind_matches(saved_preset, kind) + && inferred_preset.model == model + && saved_preset.model != model + { + inferred + } else { + saved + } +} + +fn preset_url_matches(preset: BuiltinAgentPreset, normalized: &str) -> bool { + preset.base_url.trim_end_matches('/') == normalized + || preset + .alt_base_url + .is_some_and(|url| url.trim_end_matches('/') == normalized) +} + +fn preset_kind_matches(preset: BuiltinAgentPreset, kind: BuiltinAgentKind) -> bool { + preset.kind == kind || preset.alt_kind == Some(kind) } diff --git a/crates/op-editor-core/src/lib.rs b/crates/op-editor-core/src/lib.rs index 356d105bd..fe124c102 100644 --- a/crates/op-editor-core/src/lib.rs +++ b/crates/op-editor-core/src/lib.rs @@ -79,8 +79,8 @@ pub use agent_settings::{ ImageGenProfile, ImageGenProvider, ImageSearchField, McpCli, McpServer, SettingsFocus, }; pub use agent_settings_builtin_presets::{ - builtin_agent_preset, infer_builtin_agent_preset, BuiltinAgentPreset, BuiltinAgentPresetKey, - BUILTIN_AGENT_PRESETS, + builtin_agent_preset, infer_builtin_agent_preset, normalize_builtin_agent_preset, + BuiltinAgentPreset, BuiltinAgentPresetKey, BUILTIN_AGENT_PRESETS, }; pub use align::AlignAction; pub use chat::{ diff --git a/crates/op-editor-core/src/tests_agent_settings.rs b/crates/op-editor-core/src/tests_agent_settings.rs index 76071a24d..f29876621 100644 --- a/crates/op-editor-core/src/tests_agent_settings.rs +++ b/crates/op-editor-core/src/tests_agent_settings.rs @@ -125,6 +125,26 @@ fn add_builtin_agent_prefills_ts_provider_presets_first() { ); } +#[test] +fn ark_coding_builtin_agent_infers_ark_coding_preset() { + let mut s = AgentSettings::default(); + + let id = s.add_builtin_agent_config( + "方舟CP", + "sk-test", + "ark-code-latest", + BuiltinAgentKind::Anthropic, + "https://ark.cn-beijing.volces.com/api/coding", + ); + + let agent = s + .builtin_agents + .iter() + .find(|agent| agent.id == id) + .expect("agent added"); + assert_eq!(agent.preset, BuiltinAgentPresetKey::ArkCoding); +} + #[test] fn pure_builtin_presets_do_not_toggle_api_format() { let mut anthropic = BuiltinAgentConfig { diff --git a/crates/op-host-desktop/src/settings_io.rs b/crates/op-host-desktop/src/settings_io.rs index 759958148..b103678ac 100644 --- a/crates/op-host-desktop/src/settings_io.rs +++ b/crates/op-host-desktop/src/settings_io.rs @@ -339,7 +339,17 @@ fn builtin_agent_from_payload(payload: BuiltinAgentPayload) -> Option Option { _ => return None, }) } - #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn connected_state_round_trips_through_payload() { - // Connect Claude (0) + Gemini (4), leave the rest off. - let mut src = EditorState::new(); - src.editor_ui.agent_settings.connected = [true, false, false, false, true]; - // Serialize → JSON → deserialize, the real on-disk path. - let json = serde_json::to_string(&to_payload(&src)).unwrap(); - let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); - let mut dst = EditorState::new(); - apply_payload(&mut dst, payload); - assert_eq!( - dst.editor_ui.agent_settings.connected, - [true, false, false, false, true] - ); - } - - #[test] - fn legacy_settings_without_connected_field_default_to_disconnected() { - // A settings.json written before the `connected` field - // existed must still load — the missing field defaults to - // all-disconnected rather than failing the parse. - let legacy = r#"{"version":1,"theme":"dark","locale":"en-US"}"#; - let payload: SettingsPayload = serde_json::from_str(legacy).unwrap(); - let mut dst = EditorState::new(); - apply_payload(&mut dst, payload); - assert_eq!(dst.editor_ui.agent_settings.connected, [false; 5]); - } - - #[test] - fn builtin_agents_round_trip_through_payload() { - let mut src = EditorState::new(); - src.editor_ui.agent_settings.add_builtin_agent_config( - "MiniMax", - "sk-test", - "MiniMax-M2.7", - BuiltinAgentKind::Anthropic, - "https://api.minimaxi.com/anthropic", - ); - - let json = serde_json::to_string(&to_payload(&src)).unwrap(); - let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); - let mut dst = EditorState::new(); - apply_payload(&mut dst, payload); - - assert_eq!(dst.editor_ui.agent_settings.builtin_agents.len(), 1); - assert_eq!( - dst.editor_ui.agent_settings.builtin_agents[0].display_name, - "MiniMax" - ); - assert_eq!( - dst.editor_ui.agent_settings.builtin_agents[0].api_key, - "sk-test" - ); - assert_eq!( - dst.editor_ui.agent_settings.builtin_agents[0].preset, - BuiltinAgentPresetKey::MiniMax - ); - } - - #[test] - fn duplicate_builtin_agents_are_deduped_on_load() { - let settings = r#"{ - "version": 1, - "builtin_agents": [ - { - "id": "builtin-1", - "display_name": "MINIMAX", - "kind": "openai-compat", - "api_key": "sk-test", - "model": "MiniMax-M2.7", - "base_url": "https://api.minimaxi.com/v1", - "enabled": true - }, - { - "id": "builtin-2", - "display_name": "MINIMAX", - "kind": "openai-compat", - "api_key": "sk-test", - "model": "MiniMax-M2.7", - "base_url": "https://api.minimaxi.com/v1", - "enabled": true - } - ] - }"#; - let payload: SettingsPayload = serde_json::from_str(settings).unwrap(); - let mut dst = EditorState::new(); - - apply_payload(&mut dst, payload); - - assert_eq!(dst.editor_ui.agent_settings.builtin_agents.len(), 1); - assert_eq!( - dst.editor_ui.agent_settings.builtin_agents[0].id, - "builtin-1" - ); - assert_eq!(dst.editor_ui.agent_settings.next_builtin_agent_id, 2); - } - - #[test] - fn duplicate_auto_named_builtin_agents_are_deduped_on_load() { - let settings = r#"{ - "version": 1, - "builtin_agents": [ - { - "id": "builtin-5", - "display_name": "Built-in Agent 5", - "kind": "anthropic", - "api_key": "sk-test", - "model": "claude-sonnet-4-5", - "base_url": "https://api.anthropic.com", - "enabled": true - }, - { - "id": "builtin-6", - "display_name": "Built-in Agent 6", - "kind": "anthropic", - "api_key": "sk-test", - "model": "claude-sonnet-4-5", - "base_url": "https://api.anthropic.com", - "enabled": true - } - ] - }"#; - let payload: SettingsPayload = serde_json::from_str(settings).unwrap(); - let mut dst = EditorState::new(); - - apply_payload(&mut dst, payload); - - assert_eq!(dst.editor_ui.agent_settings.builtin_agents.len(), 1); - assert_eq!( - dst.editor_ui.agent_settings.builtin_agents[0].display_name, - "Built-in Agent 5" - ); - assert_eq!(dst.editor_ui.agent_settings.next_builtin_agent_id, 6); - } - - #[test] - fn acp_agents_round_trip_through_payload() { - let mut src = EditorState::new(); - let mut env = std::collections::BTreeMap::new(); - env.insert("ACP_TOKEN".into(), "secret".into()); - src.editor_ui.agent_settings.add_acp_agent_config( - "Design Agent", - AcpConnectionType::Local, - "/usr/local/bin/design-agent", - vec!["--stdio".into()], - env, - None, - true, - ); - src.editor_ui.agent_settings.add_acp_agent_config( - "Remote Agent", - AcpConnectionType::Remote, - "", - Vec::new(), - std::collections::BTreeMap::new(), - Some("ws://localhost:8100".into()), - false, - ); - - let json = serde_json::to_string(&to_payload(&src)).unwrap(); - let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); - let mut dst = EditorState::new(); - apply_payload(&mut dst, payload); - - assert_eq!(dst.editor_ui.agent_settings.acp_agents.len(), 2); - let local = &dst.editor_ui.agent_settings.acp_agents[0]; - assert_eq!(local.display_name, "Design Agent"); - assert_eq!(local.connection_type, AcpConnectionType::Local); - assert_eq!(local.command, "/usr/local/bin/design-agent"); - assert!(!local.connected); - assert_eq!(local.args, vec!["--stdio"]); - assert_eq!( - local.env.get("ACP_TOKEN").map(String::as_str), - Some("secret") - ); - let remote = &dst.editor_ui.agent_settings.acp_agents[1]; - assert_eq!(remote.connection_type, AcpConnectionType::Remote); - assert_eq!(remote.url.as_deref(), Some("ws://localhost:8100")); - assert!(!remote.enabled); - assert_eq!(dst.editor_ui.agent_settings.next_acp_agent_id, 3); - } - - #[test] - fn image_generation_profiles_round_trip_through_payload() { - let mut src = EditorState::new(); - let first = src.editor_ui.agent_settings.add_image_gen_profile(); - let second = src.editor_ui.agent_settings.add_image_gen_profile(); - let second_profile = &mut src.editor_ui.agent_settings.image_gen_profiles[1]; - second_profile.name = "Gemini Image".into(); - second_profile.provider = ImageGenProvider::Gemini; - second_profile.api_key = "image-key".into(); - second_profile.model = "gemini-image".into(); - second_profile.base_url = Some("https://images.example/v1".into()); - assert!(src - .editor_ui - .agent_settings - .set_active_image_gen_profile(&second)); - - let json = serde_json::to_string(&to_payload(&src)).unwrap(); - let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); - let mut dst = EditorState::new(); - apply_payload(&mut dst, payload); - - assert_eq!(dst.editor_ui.agent_settings.image_gen_profiles.len(), 2); - assert_eq!( - dst.editor_ui - .agent_settings - .active_image_gen_profile_id - .as_deref(), - Some(second.as_str()) - ); - assert_eq!(dst.editor_ui.agent_settings.image_gen_profiles[0].id, first); - assert_eq!( - dst.editor_ui.agent_settings.image_gen_profiles[1].provider, - ImageGenProvider::Gemini - ); - assert_eq!( - dst.editor_ui.agent_settings.image_gen_profiles[1].base_url, - Some("https://images.example/v1".into()) - ); - } - - #[test] - fn openverse_oauth_round_trips_through_payload() { - let mut src = EditorState::new(); - src.editor_ui.agent_settings.openverse_client_id = "client-id".into(); - src.editor_ui.agent_settings.openverse_client_secret = "client-secret".into(); - - let json = serde_json::to_string(&to_payload(&src)).unwrap(); - let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); - let mut dst = EditorState::new(); - apply_payload(&mut dst, payload); - - assert_eq!( - dst.editor_ui.agent_settings.openverse_client_id, - "client-id" - ); - assert_eq!( - dst.editor_ui.agent_settings.openverse_client_secret, - "client-secret" - ); - } - - #[test] - fn auto_update_preference_round_trips_through_payload() { - let mut src = EditorState::new(); - src.editor_ui.agent_settings.auto_update_enabled = false; - - let json = serde_json::to_string(&to_payload(&src)).unwrap(); - let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); - let mut dst = EditorState::new(); - apply_payload(&mut dst, payload); - - assert!(!dst.editor_ui.agent_settings.auto_update_enabled); - } -} +#[path = "settings_io_tests.rs"] +mod settings_io_tests; diff --git a/crates/op-host-desktop/src/settings_io_tests.rs b/crates/op-host-desktop/src/settings_io_tests.rs new file mode 100644 index 000000000..09057f182 --- /dev/null +++ b/crates/op-host-desktop/src/settings_io_tests.rs @@ -0,0 +1,286 @@ +use super::*; + +#[test] +fn connected_state_round_trips_through_payload() { + // Connect Claude (0) + Gemini (4), leave the rest off. + let mut src = EditorState::new(); + src.editor_ui.agent_settings.connected = [true, false, false, false, true]; + // Serialize → JSON → deserialize, the real on-disk path. + let json = serde_json::to_string(&to_payload(&src)).unwrap(); + let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); + let mut dst = EditorState::new(); + apply_payload(&mut dst, payload); + assert_eq!( + dst.editor_ui.agent_settings.connected, + [true, false, false, false, true] + ); +} + +#[test] +fn legacy_settings_without_connected_field_default_to_disconnected() { + // A settings.json written before the `connected` field + // existed must still load — the missing field defaults to + // all-disconnected rather than failing the parse. + let legacy = r#"{"version":1,"theme":"dark","locale":"en-US"}"#; + let payload: SettingsPayload = serde_json::from_str(legacy).unwrap(); + let mut dst = EditorState::new(); + apply_payload(&mut dst, payload); + assert_eq!(dst.editor_ui.agent_settings.connected, [false; 5]); +} + +#[test] +fn builtin_agents_round_trip_through_payload() { + let mut src = EditorState::new(); + src.editor_ui.agent_settings.add_builtin_agent_config( + "MiniMax", + "sk-test", + "MiniMax-M2.7", + BuiltinAgentKind::Anthropic, + "https://api.minimaxi.com/anthropic", + ); + + let json = serde_json::to_string(&to_payload(&src)).unwrap(); + let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); + let mut dst = EditorState::new(); + apply_payload(&mut dst, payload); + + assert_eq!(dst.editor_ui.agent_settings.builtin_agents.len(), 1); + assert_eq!( + dst.editor_ui.agent_settings.builtin_agents[0].display_name, + "MiniMax" + ); + assert_eq!( + dst.editor_ui.agent_settings.builtin_agents[0].api_key, + "sk-test" + ); + assert_eq!( + dst.editor_ui.agent_settings.builtin_agents[0].preset, + BuiltinAgentPresetKey::MiniMax + ); +} + +#[test] +fn duplicate_builtin_agents_are_deduped_on_load() { + let settings = r#"{ + "version": 1, + "builtin_agents": [ + { + "id": "builtin-1", + "display_name": "MINIMAX", + "kind": "openai-compat", + "api_key": "sk-test", + "model": "MiniMax-M2.7", + "base_url": "https://api.minimaxi.com/v1", + "enabled": true + }, + { + "id": "builtin-2", + "display_name": "MINIMAX", + "kind": "openai-compat", + "api_key": "sk-test", + "model": "MiniMax-M2.7", + "base_url": "https://api.minimaxi.com/v1", + "enabled": true + } + ] + }"#; + let payload: SettingsPayload = serde_json::from_str(settings).unwrap(); + let mut dst = EditorState::new(); + + apply_payload(&mut dst, payload); + + assert_eq!(dst.editor_ui.agent_settings.builtin_agents.len(), 1); + assert_eq!( + dst.editor_ui.agent_settings.builtin_agents[0].id, + "builtin-1" + ); + assert_eq!(dst.editor_ui.agent_settings.next_builtin_agent_id, 2); +} + +#[test] +fn duplicate_auto_named_builtin_agents_are_deduped_on_load() { + let settings = r#"{ + "version": 1, + "builtin_agents": [ + { + "id": "builtin-5", + "display_name": "Built-in Agent 5", + "kind": "anthropic", + "api_key": "sk-test", + "model": "claude-sonnet-4-5", + "base_url": "https://api.anthropic.com", + "enabled": true + }, + { + "id": "builtin-6", + "display_name": "Built-in Agent 6", + "kind": "anthropic", + "api_key": "sk-test", + "model": "claude-sonnet-4-5", + "base_url": "https://api.anthropic.com", + "enabled": true + } + ] + }"#; + let payload: SettingsPayload = serde_json::from_str(settings).unwrap(); + let mut dst = EditorState::new(); + + apply_payload(&mut dst, payload); + + assert_eq!(dst.editor_ui.agent_settings.builtin_agents.len(), 1); + assert_eq!( + dst.editor_ui.agent_settings.builtin_agents[0].display_name, + "Built-in Agent 5" + ); + assert_eq!(dst.editor_ui.agent_settings.next_builtin_agent_id, 6); +} + +#[test] +fn acp_agents_round_trip_through_payload() { + let mut src = EditorState::new(); + let mut env = std::collections::BTreeMap::new(); + env.insert("ACP_TOKEN".into(), "secret".into()); + src.editor_ui.agent_settings.add_acp_agent_config( + "Design Agent", + AcpConnectionType::Local, + "/usr/local/bin/design-agent", + vec!["--stdio".into()], + env, + None, + true, + ); + src.editor_ui.agent_settings.add_acp_agent_config( + "Remote Agent", + AcpConnectionType::Remote, + "", + Vec::new(), + std::collections::BTreeMap::new(), + Some("ws://localhost:8100".into()), + false, + ); + + let json = serde_json::to_string(&to_payload(&src)).unwrap(); + let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); + let mut dst = EditorState::new(); + apply_payload(&mut dst, payload); + + assert_eq!(dst.editor_ui.agent_settings.acp_agents.len(), 2); + let local = &dst.editor_ui.agent_settings.acp_agents[0]; + assert_eq!(local.display_name, "Design Agent"); + assert_eq!(local.connection_type, AcpConnectionType::Local); + assert_eq!(local.command, "/usr/local/bin/design-agent"); + assert!(!local.connected); + assert_eq!(local.args, vec!["--stdio"]); + assert_eq!( + local.env.get("ACP_TOKEN").map(String::as_str), + Some("secret") + ); + let remote = &dst.editor_ui.agent_settings.acp_agents[1]; + assert_eq!(remote.connection_type, AcpConnectionType::Remote); + assert_eq!(remote.url.as_deref(), Some("ws://localhost:8100")); + assert!(!remote.enabled); + assert_eq!(dst.editor_ui.agent_settings.next_acp_agent_id, 3); +} + +#[test] +fn image_generation_profiles_round_trip_through_payload() { + let mut src = EditorState::new(); + let first = src.editor_ui.agent_settings.add_image_gen_profile(); + let second = src.editor_ui.agent_settings.add_image_gen_profile(); + let second_profile = &mut src.editor_ui.agent_settings.image_gen_profiles[1]; + second_profile.name = "Gemini Image".into(); + second_profile.provider = ImageGenProvider::Gemini; + second_profile.api_key = "image-key".into(); + second_profile.model = "gemini-image".into(); + second_profile.base_url = Some("https://images.example/v1".into()); + assert!(src + .editor_ui + .agent_settings + .set_active_image_gen_profile(&second)); + + let json = serde_json::to_string(&to_payload(&src)).unwrap(); + let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); + let mut dst = EditorState::new(); + apply_payload(&mut dst, payload); + + assert_eq!(dst.editor_ui.agent_settings.image_gen_profiles.len(), 2); + assert_eq!( + dst.editor_ui + .agent_settings + .active_image_gen_profile_id + .as_deref(), + Some(second.as_str()) + ); + assert_eq!(dst.editor_ui.agent_settings.image_gen_profiles[0].id, first); + assert_eq!( + dst.editor_ui.agent_settings.image_gen_profiles[1].provider, + ImageGenProvider::Gemini + ); + assert_eq!( + dst.editor_ui.agent_settings.image_gen_profiles[1].base_url, + Some("https://images.example/v1".into()) + ); +} + +#[test] +fn openverse_oauth_round_trips_through_payload() { + let mut src = EditorState::new(); + src.editor_ui.agent_settings.openverse_client_id = "client-id".into(); + src.editor_ui.agent_settings.openverse_client_secret = "client-secret".into(); + + let json = serde_json::to_string(&to_payload(&src)).unwrap(); + let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); + let mut dst = EditorState::new(); + apply_payload(&mut dst, payload); + + assert_eq!( + dst.editor_ui.agent_settings.openverse_client_id, + "client-id" + ); + assert_eq!( + dst.editor_ui.agent_settings.openverse_client_secret, + "client-secret" + ); +} + +#[test] +fn auto_update_preference_round_trips_through_payload() { + let mut src = EditorState::new(); + src.editor_ui.agent_settings.auto_update_enabled = false; + + let json = serde_json::to_string(&to_payload(&src)).unwrap(); + let payload: SettingsPayload = serde_json::from_str(&json).unwrap(); + let mut dst = EditorState::new(); + apply_payload(&mut dst, payload); + + assert!(!dst.editor_ui.agent_settings.auto_update_enabled); +} + +#[test] +fn legacy_ark_coding_payload_with_doubao_preset_migrates_to_ark_coding() { + let settings = r#"{ + "version": 1, + "builtin_agents": [ + { + "id": "builtin-3", + "preset": "doubao", + "display_name": "方舟CP", + "kind": "anthropic", + "api_key": "sk-test", + "model": "ark-code-latest", + "base_url": "https://ark.cn-beijing.volces.com/api/coding", + "enabled": true + } + ] + }"#; + let payload: SettingsPayload = serde_json::from_str(settings).unwrap(); + let mut dst = EditorState::new(); + + apply_payload(&mut dst, payload); + + assert_eq!(dst.editor_ui.agent_settings.builtin_agents.len(), 1); + assert_eq!( + dst.editor_ui.agent_settings.builtin_agents[0].preset, + BuiltinAgentPresetKey::ArkCoding + ); +}