From e78aa8a7c3fd2a611f377c6b8a16127463b5cd48 Mon Sep 17 00:00:00 2001 From: Allen Xu Date: Wed, 8 Jul 2026 12:09:56 +0800 Subject: [PATCH] fix: sync openclaw and hermes live provider updates (#5098) * fix: sync openclaw live provider updates * fix: sync hermes live provider updates * fix test: hermes live import stores models as array after denormalize The test import_hermes_providers_from_live_updates_existing_provider_from_live seeded models as a dict, but import_hermes_providers_from_live reads via get_providers() which calls denormalize_provider_models_for_read(), converting models from YAML dict to UI-friendly array. The test assertion accessed models as dict -> Null -> assertion failure. Fix: access models as array by index, matching the actual storage format after live import. Also verify the id field is preserved in the denormalized output. --- src-tauri/src/lib.rs | 8 +- src-tauri/src/services/provider/live.rs | 58 ++++++++++--- src-tauri/src/services/provider/mod.rs | 109 ++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9737a52da..98ff01073 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -689,16 +689,16 @@ pub fn run() { } match crate::services::provider::import_openclaw_providers_from_live(&app_state) { Ok(count) if count > 0 => { - log::info!("✓ Imported {count} OpenClaw provider(s) from live config"); + log::info!("✓ Synced {count} OpenClaw provider(s) from live config"); } - Ok(_) => log::debug!("○ No new OpenClaw providers to import"), + Ok(_) => log::debug!("○ No OpenClaw provider changes from live config"), Err(e) => log::warn!("✗ Failed to import OpenClaw providers: {e}"), } match crate::services::provider::import_hermes_providers_from_live(&app_state) { Ok(count) if count > 0 => { - log::info!("✓ Imported {count} Hermes provider(s) from live config"); + log::info!("✓ Synced {count} Hermes provider(s) from live config"); } - Ok(_) => log::debug!("○ No new Hermes providers to import"), + Ok(_) => log::debug!("○ No Hermes provider changes from live config"), Err(e) => log::warn!("✗ Failed to import Hermes providers: {e}"), } diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index d289e289c..961df149c 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -1479,6 +1479,7 @@ pub fn import_openclaw_providers_from_live(state: &AppState) -> Result Result v, @@ -1507,6 +1502,30 @@ pub fn import_openclaw_providers_from_live(state: &AppState) -> Result { + if existing.settings_config != settings_config { + let mut provider = existing; + provider.settings_config = settings_config; + if let Err(e) = state.db.save_provider("openclaw", &provider) { + log::warn!( + "Failed to update OpenClaw provider '{id}' from live config: {e}" + ); + } else { + updated += 1; + log::info!("Updated OpenClaw provider '{id}' from live config"); + } + } + } + Ok(None) => { + log::warn!("OpenClaw provider '{id}' disappeared while importing live config") + } + Err(e) => log::warn!("Failed to look up OpenClaw provider '{id}': {e}"), + } + continue; + } + // Determine display name: use first model name if available, otherwise use id let display_name = config .models @@ -1531,7 +1550,7 @@ pub fn import_openclaw_providers_from_live(state: &AppState) -> Result Result Result { + if existing.settings_config != config { + let mut provider = existing; + provider.settings_config = config; + if let Err(e) = state.db.save_provider("hermes", &provider) { + log::warn!( + "Failed to update Hermes provider '{name}' from live config: {e}" + ); + } else { + updated += 1; + log::info!("Updated Hermes provider '{name}' from live config"); + } + } + } + Ok(None) => { + log::warn!("Hermes provider '{name}' disappeared while importing live config") + } + Err(e) => log::warn!("Failed to look up Hermes provider '{name}': {e}"), + } continue; } @@ -1580,7 +1618,7 @@ pub fn import_hermes_providers_from_live(state: &AppState) -> Result Provider { + Provider { + id: id.to_string(), + name: format!("Provider {id}"), + settings_config: json!({ + "api": "openai-chat", + "base_url": "https://api.example.com/v1", + "api_key": "test-key", + "models": { + "gpt-4o": { + "name": "GPT-4o" + } + } + }), + website_url: None, + category: Some("custom".to_string()), + created_at: Some(1), + sort_index: Some(0), + notes: None, + meta: None, + icon: None, + icon_color: None, + in_failover_queue: false, + } + } + fn opencode_provider(id: &str) -> Provider { Provider { id: id.to_string(), @@ -1362,6 +1388,89 @@ base_url = "http://localhost:8080" }); } + #[test] + #[serial] + fn import_openclaw_providers_from_live_updates_existing_provider_from_live() { + with_test_home(|state, _| { + let mut provider = openclaw_provider("existing-openclaw"); + provider.settings_config["models"] = json!([ + { + "id": "claude-sonnet-4", + "name": "Claude Sonnet 4" + } + ]); + state + .db + .save_provider(AppType::OpenClaw.as_str(), &provider) + .expect("seed existing openclaw provider"); + + let mut live_settings = provider.settings_config.clone(); + live_settings["baseUrl"] = Value::String("https://api.example.com/v1".to_string()); + live_settings["models"][0]["name"] = Value::String("Claude Sonnet 4.1".to_string()); + crate::openclaw_config::set_provider(&provider.id, live_settings) + .expect("seed edited live openclaw provider"); + + let updated = import_openclaw_providers_from_live(state) + .expect("import openclaw providers from live"); + assert_eq!(updated, 1); + + let saved = state + .db + .get_provider_by_id(&provider.id, AppType::OpenClaw.as_str()) + .expect("query updated openclaw provider") + .expect("openclaw provider should exist"); + assert_eq!(saved.name, provider.name); + assert_eq!( + saved.settings_config["baseUrl"], + json!("https://api.example.com/v1") + ); + assert_eq!( + saved.settings_config["models"][0]["name"], + json!("Claude Sonnet 4.1") + ); + }); + } + + #[test] + #[serial] + fn import_hermes_providers_from_live_updates_existing_provider_from_live() { + with_test_home(|state, _| { + let provider = hermes_provider("existing-hermes"); + state + .db + .save_provider(AppType::Hermes.as_str(), &provider) + .expect("seed existing hermes provider"); + + let mut live_settings = provider.settings_config.clone(); + live_settings["base_url"] = Value::String("https://api.hermes.example/v1".to_string()); + live_settings["models"]["gpt-4o"]["name"] = Value::String("GPT-4o Updated".to_string()); + crate::hermes_config::set_provider(&provider.id, live_settings) + .expect("seed edited live hermes provider"); + + let updated = import_hermes_providers_from_live(state) + .expect("import hermes providers from live"); + assert_eq!(updated, 1); + + let saved = state + .db + .get_provider_by_id(&provider.id, AppType::Hermes.as_str()) + .expect("query updated hermes provider") + .expect("hermes provider should exist"); + assert_eq!(saved.name, provider.name); + assert_eq!( + saved.settings_config["base_url"], + json!("https://api.hermes.example/v1") + ); + // models are denormalized from YAML dict to UI-friendly array by + // get_providers(), so access by index rather than dict key + assert_eq!( + saved.settings_config["models"][0]["name"], + json!("GPT-4o Updated") + ); + assert_eq!(saved.settings_config["models"][0]["id"], json!("gpt-4o")); + }); + } + #[test] #[serial] fn legacy_additive_provider_still_errors_on_live_config_parse_failure() {