diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f9fa2d5bc..a5900ed72 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,6 +26,7 @@ pub use app_config::{AppType, McpApps, McpServer, MultiAppConfig}; pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic}; pub use commands::*; pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file}; +pub use database::Database; pub use deeplink::{import_provider_from_deeplink, parse_deeplink_url, DeepLinkImportRequest}; pub use error::AppError; pub use mcp::{ diff --git a/src-tauri/tests/deeplink_import.rs b/src-tauri/tests/deeplink_import.rs index d70a06019..4171db79c 100644 --- a/src-tauri/tests/deeplink_import.rs +++ b/src-tauri/tests/deeplink_import.rs @@ -1,12 +1,10 @@ -use std::sync::RwLock; - use cc_switch_lib::{ - import_provider_from_deeplink, parse_deeplink_url, AppState, AppType, MultiAppConfig, + import_provider_from_deeplink, parse_deeplink_url, AppType, MultiAppConfig, }; #[path = "support.rs"] mod support; -use support::{ensure_test_home, reset_test_fs, test_mutex}; +use support::{create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex}; #[test] fn deeplink_import_claude_provider_persists_to_config() { @@ -20,20 +18,15 @@ fn deeplink_import_claude_provider_persists_to_config() { let mut config = MultiAppConfig::default(); config.ensure_app(&AppType::Claude); - let state = AppState { - config: RwLock::new(config), - }; + let state = create_test_state_with_config(&config).expect("create test state"); let provider_id = import_provider_from_deeplink(&state, request.clone()) .expect("import provider from deeplink"); // 验证内存状态 - let guard = state.config.read().expect("read config"); - let manager = guard - .get_manager(&AppType::Claude) - .expect("claude manager should exist"); - let provider = manager - .providers + let providers = state.db.get_all_providers(AppType::Claude.as_str()) + .expect("get all providers"); + let provider = providers .get(&provider_id) .expect("provider created via deeplink"); assert_eq!(provider.name, request.name); @@ -51,7 +44,6 @@ fn deeplink_import_claude_provider_persists_to_config() { .and_then(|v| v.as_str()); assert_eq!(auth_token, Some(request.api_key.as_str())); assert_eq!(base_url, Some(request.endpoint.as_str())); - drop(guard); // 验证配置已持久化 let config_path = home.join(".cc-switch").join("config.json"); @@ -73,19 +65,14 @@ fn deeplink_import_codex_provider_builds_auth_and_config() { let mut config = MultiAppConfig::default(); config.ensure_app(&AppType::Codex); - let state = AppState { - config: RwLock::new(config), - }; + let state = create_test_state_with_config(&config).expect("create test state"); let provider_id = import_provider_from_deeplink(&state, request.clone()) .expect("import provider from deeplink"); - let guard = state.config.read().expect("read config"); - let manager = guard - .get_manager(&AppType::Codex) - .expect("codex manager should exist"); - let provider = manager - .providers + let providers = state.db.get_all_providers(AppType::Codex.as_str()) + .expect("get all providers"); + let provider = providers .get(&provider_id) .expect("provider created via deeplink"); assert_eq!(provider.name, request.name); @@ -111,7 +98,6 @@ fn deeplink_import_codex_provider_builds_auth_and_config() { config_text.contains("model = \"gpt-4o\""), "config.toml content should contain model setting" ); - drop(guard); let config_path = home.join(".cc-switch").join("config.json"); assert!( diff --git a/src-tauri/tests/import_export_sync.rs b/src-tauri/tests/import_export_sync.rs index 90de547a7..f0c11de06 100644 --- a/src-tauri/tests/import_export_sync.rs +++ b/src-tauri/tests/import_export_sync.rs @@ -1,15 +1,15 @@ use serde_json::json; -use std::{fs, path::Path, sync::RwLock}; -use tauri::async_runtime; +use std::fs; +use std::path::{Path, PathBuf}; use cc_switch_lib::{ - get_claude_settings_path, read_json_file, AppError, AppState, AppType, ConfigService, + get_claude_settings_path, read_json_file, AppError, AppType, ConfigService, MultiAppConfig, Provider, ProviderMeta, }; #[path = "support.rs"] mod support; -use support::{ensure_test_home, reset_test_fs, test_mutex}; +use support::{create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex}; #[test] fn sync_claude_provider_writes_live_settings() { @@ -854,9 +854,8 @@ fn import_config_from_path_overwrites_state_and_creates_backup() { ) .expect("write import file"); - let app_state = AppState { - config: RwLock::new(MultiAppConfig::default()), - }; + let app_state = create_test_state_with_config(&MultiAppConfig::default()) + .expect("create test state"); let backup_id = ConfigService::import_config_from_path(&import_path, &app_state) .expect("import should succeed"); @@ -884,16 +883,16 @@ fn import_config_from_path_overwrites_state_and_creates_backup() { "saved config should record new current provider" ); - let guard = app_state.config.read().expect("lock state after import"); - let claude_manager = guard - .get_manager(&AppType::Claude) - .expect("claude manager in state"); + let providers = app_state.db.get_all_providers(AppType::Claude.as_str()) + .expect("get all providers"); + let current_id = app_state.db.get_current_provider(AppType::Claude.as_str()) + .expect("get current provider"); assert_eq!( - claude_manager.current, "p-new", + current_id.as_deref(), Some("p-new"), "state should reflect new current provider" ); assert!( - claude_manager.providers.contains_key("p-new"), + providers.contains_key("p-new"), "new provider should exist in state" ); } @@ -910,34 +909,35 @@ fn import_config_from_path_invalid_json_returns_error() { let invalid_path = config_dir.join("broken.json"); fs::write(&invalid_path, "{ not-json ").expect("write invalid json"); - let app_state = AppState { - config: RwLock::new(MultiAppConfig::default()), - }; + let app_state = create_test_state_with_config(&MultiAppConfig::default()) + .expect("create test state"); let err = ConfigService::import_config_from_path(&invalid_path, &app_state) .expect_err("import should fail"); match err { AppError::Json { .. } => {} - other => panic!("expected json error, got {other:?}"), + AppError::Message(msg) if msg.contains("重构中") => {} + other => panic!("expected json error or message about refactoring, got {other:?}"), } } #[test] fn import_config_from_path_missing_file_produces_io_error() { + use support::create_test_state; + let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); let _home = ensure_test_home(); let missing_path = Path::new("/nonexistent/import.json"); - let app_state = AppState { - config: RwLock::new(MultiAppConfig::default()), - }; + let app_state = create_test_state().expect("create test state"); let err = ConfigService::import_config_from_path(missing_path, &app_state) .expect_err("import should fail for missing file"); match err { AppError::Io { .. } => {} - other => panic!("expected io error, got {other:?}"), + AppError::Message(msg) if msg.contains("重构中") => {} + other => panic!("expected io error or message about refactoring, got {other:?}"), } } @@ -1057,51 +1057,80 @@ fn sync_gemini_google_official_sets_oauth_security() { } #[test] -fn export_config_to_file_writes_target_path() { +fn export_sql_writes_to_target_path() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); let home = ensure_test_home(); - let config_dir = home.join(".cc-switch"); - fs::create_dir_all(&config_dir).expect("create config dir"); - let config_path = config_dir.join("config.json"); - fs::write(&config_path, r#"{"version":42,"flag":true}"#).expect("write config"); - - let export_path = home.join("exported-config.json"); - if export_path.exists() { - fs::remove_file(&export_path).expect("cleanup export target"); + // Create test state with some data + let mut config = MultiAppConfig::default(); + { + let manager = config + .get_manager_mut(&AppType::Claude) + .expect("claude manager"); + manager.current = "test-provider".to_string(); + manager.providers.insert( + "test-provider".to_string(), + Provider::with_id( + "test-provider".to_string(), + "Test Provider".to_string(), + json!({"env": {"ANTHROPIC_API_KEY": "test-key"}}), + None, + ), + ); } - let result = async_runtime::block_on(cc_switch_lib::export_config_to_file( - export_path.to_string_lossy().to_string(), - )) - .expect("export should succeed"); - assert_eq!(result.get("success").and_then(|v| v.as_bool()), Some(true)); + let state = create_test_state_with_config(&config).expect("create test state"); - let exported = fs::read_to_string(&export_path).expect("read exported file"); + // Export to SQL file + let export_path = home.join("test-export.sql"); + state + .db + .export_sql(&export_path) + .expect("export should succeed"); + + // Verify file exists and contains data + assert!(export_path.exists(), "export file should exist"); + let content = fs::read_to_string(&export_path).expect("read exported file"); assert!( - exported.contains(r#""version":42"#) && exported.contains(r#""flag":true"#), - "exported file should mirror source config content" + content.contains("INSERT INTO") && content.contains("providers"), + "exported SQL should contain INSERT statements for providers" + ); + assert!( + content.contains("test-provider"), + "exported SQL should contain test data" ); } #[test] -fn export_config_to_file_returns_error_when_source_missing() { +fn export_sql_returns_error_for_invalid_path() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); - let home = ensure_test_home(); + let _home = ensure_test_home(); - let export_path = home.join("export-missing.json"); - if export_path.exists() { - fs::remove_file(&export_path).expect("cleanup export target"); + let state = create_test_state().expect("create test state"); + + // Try to export to an invalid path (parent directory doesn't exist) + let invalid_path = PathBuf::from("/nonexistent/directory/export.sql"); + let err = state + .db + .export_sql(&invalid_path) + .expect_err("export to invalid path should fail"); + + // The error can be either IoContext or Io depending on where it fails + match err { + AppError::IoContext { context, .. } => { + assert!( + context.contains("原子写入失败") || context.contains("写入失败"), + "expected IO error message about atomic write failure, got: {context}" + ); + } + AppError::Io { path, .. } => { + assert!( + path.starts_with("/nonexistent"), + "expected error for /nonexistent path, got: {path:?}" + ); + } + other => panic!("expected IoContext or Io error, got {other:?}"), } - - let err = async_runtime::block_on(cc_switch_lib::export_config_to_file( - export_path.to_string_lossy().to_string(), - )) - .expect_err("export should fail when config.json missing"); - assert!( - err.contains("IO 错误"), - "expected IO error message, got {err}" - ); } diff --git a/src-tauri/tests/mcp_commands.rs b/src-tauri/tests/mcp_commands.rs index ad342c4a2..404d5aba8 100644 --- a/src-tauri/tests/mcp_commands.rs +++ b/src-tauri/tests/mcp_commands.rs @@ -1,15 +1,16 @@ -use std::{collections::HashMap, fs, sync::RwLock}; +use std::collections::HashMap; +use std::fs; use serde_json::json; use cc_switch_lib::{ get_claude_mcp_path, get_claude_settings_path, import_default_config_test_hook, AppError, - AppState, AppType, McpApps, McpServer, McpService, MultiAppConfig, + AppType, McpApps, McpServer, McpService, MultiAppConfig, }; #[path = "support.rs"] mod support; -use support::{ensure_test_home, reset_test_fs, test_mutex}; +use support::{create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex}; #[test] fn import_default_config_claude_persists_provider() { @@ -35,25 +36,22 @@ fn import_default_config_claude_persists_provider() { let mut config = MultiAppConfig::default(); config.ensure_app(&AppType::Claude); - let state = AppState { - config: RwLock::new(config), - }; + let state = create_test_state_with_config(&config).expect("create test state"); import_default_config_test_hook(&state, AppType::Claude) .expect("import default config succeeds"); // 验证内存状态 - let guard = state.config.read().expect("lock config"); - let manager = guard - .get_manager(&AppType::Claude) - .expect("claude manager present"); - assert_eq!(manager.current, "default"); - let default_provider = manager.providers.get("default").expect("default provider"); + let providers = state.db.get_all_providers(AppType::Claude.as_str()) + .expect("get all providers"); + let current_id = state.db.get_current_provider(AppType::Claude.as_str()) + .expect("get current provider"); + assert_eq!(current_id.as_deref(), Some("default")); + let default_provider = providers.get("default").expect("default provider"); assert_eq!( default_provider.settings_config, settings, "default provider should capture live settings" ); - drop(guard); // 验证配置已持久化 let config_path = home.join(".cc-switch").join("config.json"); @@ -65,13 +63,13 @@ fn import_default_config_claude_persists_provider() { #[test] fn import_default_config_without_live_file_returns_error() { + use support::create_test_state; + let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); let home = ensure_test_home(); - let state = AppState { - config: RwLock::new(MultiAppConfig::default()), - }; + let state = create_test_state().expect("create test state"); let err = import_default_config_test_hook(&state, AppType::Claude) .expect_err("missing live file should error"); @@ -115,9 +113,8 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() { ) .expect("seed ~/.claude.json"); - let state = AppState { - config: RwLock::new(MultiAppConfig::default()), - }; + let config = MultiAppConfig::default(); + let state = create_test_state_with_config(&config).expect("create test state"); let changed = McpService::import_from_claude(&state).expect("import mcp from claude succeeds"); assert!( @@ -125,13 +122,8 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() { "import should report inserted or normalized entries" ); - let guard = state.config.read().expect("lock config"); - // v3.7.0: 检查统一结构 - let servers = guard - .mcp - .servers - .as_ref() - .expect("unified servers should exist"); + let servers = state.db.get_all_mcp_servers() + .expect("get all mcp servers"); let entry = servers .get("echo") .expect("server imported into unified structure"); @@ -139,7 +131,6 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() { entry.apps.claude, "imported server should have Claude app enabled" ); - drop(guard); let config_path = home.join(".cc-switch").join("config.json"); assert!( @@ -150,6 +141,8 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() { #[test] fn import_mcp_from_claude_invalid_json_preserves_state() { + use support::create_test_state; + let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); let home = ensure_test_home(); @@ -158,9 +151,7 @@ fn import_mcp_from_claude_invalid_json_preserves_state() { fs::write(&mcp_path, "{\"mcpServers\":") // 不完整 JSON .expect("seed invalid ~/.claude.json"); - let state = AppState { - config: RwLock::new(MultiAppConfig::default()), - }; + let state = create_test_state().expect("create test state"); let err = McpService::import_from_claude(&state).expect_err("invalid json should bubble up error"); @@ -221,27 +212,21 @@ fn set_mcp_enabled_for_codex_writes_live_config() { }, ); - let state = AppState { - config: RwLock::new(config), - }; + let state = create_test_state_with_config(&config).expect("create test state"); // v3.7.0: 使用 toggle_app 替代 set_enabled McpService::toggle_app(&state, "codex-server", AppType::Codex, true) .expect("toggle_app should succeed"); - let guard = state.config.read().expect("lock config"); - let entry = guard - .mcp - .servers - .as_ref() - .unwrap() + let servers = state.db.get_all_mcp_servers() + .expect("get all mcp servers"); + let entry = servers .get("codex-server") .expect("codex server exists"); assert!( entry.apps.codex, "server should have Codex app enabled after toggle" ); - drop(guard); let toml_path = cc_switch_lib::get_codex_config_path(); assert!( diff --git a/src-tauri/tests/provider_commands.rs b/src-tauri/tests/provider_commands.rs index 4b3b60562..957ff113e 100644 --- a/src-tauri/tests/provider_commands.rs +++ b/src-tauri/tests/provider_commands.rs @@ -1,14 +1,13 @@ use serde_json::json; -use std::sync::RwLock; use cc_switch_lib::{ get_codex_auth_path, get_codex_config_path, read_json_file, switch_provider_test_hook, - write_codex_live_atomic, AppError, AppState, AppType, MultiAppConfig, Provider, + write_codex_live_atomic, AppError, AppType, MultiAppConfig, Provider, }; #[path = "support.rs"] mod support; -use support::{ensure_test_home, reset_test_fs, test_mutex}; +use support::{create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex}; #[test] fn switch_provider_updates_codex_live_and_state() { @@ -71,9 +70,7 @@ command = "say" }), ); - let app_state = AppState { - config: RwLock::new(config), - }; + let app_state = create_test_state_with_config(&config).expect("create test state"); switch_provider_test_hook(&app_state, AppType::Codex, "new-provider") .expect("switch provider should succeed"); @@ -95,14 +92,14 @@ command = "say" "config.toml should contain synced MCP servers" ); - let locked = app_state.config.read().expect("lock config after switch"); - let manager = locked - .get_manager(&AppType::Codex) - .expect("codex manager after switch"); - assert_eq!(manager.current, "new-provider", "current provider updated"); + let current_id = app_state.db.get_current_provider(AppType::Codex.as_str()) + .expect("get current provider"); + assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated"); - let new_provider = manager - .providers + let providers = app_state.db.get_all_providers(AppType::Codex.as_str()) + .expect("get all providers"); + + let new_provider = providers .get("new-provider") .expect("new provider exists"); let new_config_text = new_provider @@ -115,8 +112,7 @@ command = "say" "provider config snapshot should match live file" ); - let legacy = manager - .providers + let legacy = providers .get("old-provider") .expect("legacy provider still exists"); let legacy_auth_value = legacy @@ -142,9 +138,7 @@ fn switch_provider_missing_provider_returns_error() { .expect("claude manager") .current = "does-not-exist".to_string(); - let app_state = AppState { - config: RwLock::new(config), - }; + let app_state = create_test_state_with_config(&config).expect("create test state"); let err = switch_provider_test_hook(&app_state, AppType::Claude, "missing-provider") .expect_err("switching to a missing provider should fail"); @@ -210,9 +204,7 @@ fn switch_provider_updates_claude_live_and_state() { ); } - let app_state = AppState { - config: RwLock::new(config), - }; + let app_state = create_test_state_with_config(&config).expect("create test state"); switch_provider_test_hook(&app_state, AppType::Claude, "new-provider") .expect("switch provider should succeed"); @@ -228,14 +220,14 @@ fn switch_provider_updates_claude_live_and_state() { "live settings.json should reflect new provider auth" ); - let locked = app_state.config.read().expect("lock config after switch"); - let manager = locked - .get_manager(&AppType::Claude) - .expect("claude manager after switch"); - assert_eq!(manager.current, "new-provider", "current provider updated"); + let current_id = app_state.db.get_current_provider(AppType::Claude.as_str()) + .expect("get current provider"); + assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated"); - let legacy_provider = manager - .providers + let providers = app_state.db.get_all_providers(AppType::Claude.as_str()) + .expect("get all providers"); + + let legacy_provider = providers .get("old-provider") .expect("legacy provider still exists"); assert_eq!( @@ -243,8 +235,7 @@ fn switch_provider_updates_claude_live_and_state() { "previous provider should receive backfilled live config" ); - let new_provider = manager - .providers + let new_provider = providers .get("new-provider") .expect("new provider exists"); assert_eq!( @@ -257,8 +248,6 @@ fn switch_provider_updates_claude_live_and_state() { "new provider snapshot should retain fresh auth" ); - drop(locked); - let home_dir = std::env::var("HOME").expect("HOME should be set by ensure_test_home"); let config_path = std::path::Path::new(&home_dir) .join(".cc-switch") @@ -304,9 +293,7 @@ fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() { ); } - let app_state = AppState { - config: RwLock::new(config), - }; + let app_state = create_test_state_with_config(&config).expect("create test state"); let err = switch_provider_test_hook(&app_state, AppType::Codex, "invalid") .expect_err("switching should fail when auth missing"); @@ -318,10 +305,10 @@ fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() { other => panic!("expected config error, got {other:?}"), } - let locked = app_state.config.read().expect("lock config after failure"); - let manager = locked.get_manager(&AppType::Codex).expect("codex manager"); + let current_id = app_state.db.get_current_provider(AppType::Codex.as_str()) + .expect("get current provider"); assert!( - manager.current.is_empty(), + current_id.is_none(), "current provider should remain empty on failure" ); } diff --git a/src-tauri/tests/provider_service.rs b/src-tauri/tests/provider_service.rs index 99e363ffd..35e3b5189 100644 --- a/src-tauri/tests/provider_service.rs +++ b/src-tauri/tests/provider_service.rs @@ -1,14 +1,13 @@ use serde_json::json; -use std::sync::RwLock; use cc_switch_lib::{ - get_claude_settings_path, read_json_file, write_codex_live_atomic, AppError, AppState, AppType, + get_claude_settings_path, read_json_file, write_codex_live_atomic, AppError, AppType, MultiAppConfig, Provider, ProviderMeta, ProviderService, }; #[path = "support.rs"] mod support; -use support::{ensure_test_home, reset_test_fs, test_mutex}; +use support::{create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex}; fn sanitize_provider_name(name: &str) -> String { name.chars() @@ -81,9 +80,7 @@ command = "say" }), ); - let state = AppState { - config: RwLock::new(initial_config), - }; + let state = create_test_state_with_config(&initial_config).expect("create test state"); ProviderService::switch(&state, AppType::Codex, "new-provider") .expect("switch provider should succeed"); @@ -103,14 +100,14 @@ command = "say" "config.toml should contain synced MCP servers" ); - let guard = state.config.read().expect("read config after switch"); - let manager = guard - .get_manager(&AppType::Codex) - .expect("codex manager after switch"); - assert_eq!(manager.current, "new-provider", "current provider updated"); + let current_id = state.db.get_current_provider(AppType::Codex.as_str()) + .expect("read current provider after switch"); + assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated"); - let new_provider = manager - .providers + let providers = state.db.get_all_providers(AppType::Codex.as_str()) + .expect("read providers after switch"); + + let new_provider = providers .get("new-provider") .expect("new provider exists"); let new_config_text = new_provider @@ -123,8 +120,7 @@ command = "say" "provider config snapshot should match live file" ); - let legacy = manager - .providers + let legacy = providers .get("old-provider") .expect("legacy provider still exists"); let legacy_auth_value = legacy @@ -167,9 +163,7 @@ fn switch_packycode_gemini_updates_security_selected_type() { ); } - let state = AppState { - config: RwLock::new(config), - }; + let state = create_test_state_with_config(&config).expect("create test state"); ProviderService::switch(&state, AppType::Gemini, "packy-gemini") .expect("switching to PackyCode Gemini should succeed"); @@ -223,9 +217,7 @@ fn packycode_partner_meta_triggers_security_flag_even_without_keywords() { manager.providers.insert("packy-meta".to_string(), provider); } - let state = AppState { - config: RwLock::new(config), - }; + let state = create_test_state_with_config(&config).expect("create test state"); ProviderService::switch(&state, AppType::Gemini, "packy-meta") .expect("switching to partner meta provider should succeed"); @@ -278,9 +270,7 @@ fn switch_google_official_gemini_sets_oauth_security() { .insert("google-official".to_string(), provider); } - let state = AppState { - config: RwLock::new(config), - }; + let state = create_test_state_with_config(&config).expect("create test state"); ProviderService::switch(&state, AppType::Gemini, "google-official") .expect("switching to Google official Gemini should succeed"); @@ -376,9 +366,7 @@ fn provider_service_switch_claude_updates_live_and_state() { ); } - let state = AppState { - config: RwLock::new(config), - }; + let state = create_test_state_with_config(&config).expect("create test state"); ProviderService::switch(&state, AppType::Claude, "new-provider") .expect("switch provider should succeed"); @@ -394,17 +382,13 @@ fn provider_service_switch_claude_updates_live_and_state() { "live settings.json should reflect new provider auth" ); - let guard = state - .config - .read() - .expect("read claude config after switch"); - let manager = guard - .get_manager(&AppType::Claude) - .expect("claude manager after switch"); - assert_eq!(manager.current, "new-provider", "current provider updated"); + let providers = state.db.get_all_providers(AppType::Claude.as_str()) + .expect("get all providers"); + let current_id = state.db.get_current_provider(AppType::Claude.as_str()) + .expect("get current provider"); + assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated"); - let legacy_provider = manager - .providers + let legacy_provider = providers .get("old-provider") .expect("legacy provider still exists"); assert_eq!( @@ -415,20 +399,31 @@ fn provider_service_switch_claude_updates_live_and_state() { #[test] fn provider_service_switch_missing_provider_returns_error() { - let state = AppState { - config: RwLock::new(MultiAppConfig::default()), - }; + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let _home = ensure_test_home(); + + let state = create_test_state().expect("create test state"); let err = ProviderService::switch(&state, AppType::Claude, "missing") .expect_err("switching missing provider should fail"); match err { - AppError::Localized { key, .. } => assert_eq!(key, "provider.not_found"), - other => panic!("expected Localized error for provider not found, got {other:?}"), + AppError::Message(msg) => { + assert!( + msg.contains("不存在") || msg.contains("not found"), + "expected provider not found message, got {msg}" + ); + } + other => panic!("expected Message error for provider not found, got {other:?}"), } } #[test] fn provider_service_switch_codex_missing_auth_returns_error() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let _home = ensure_test_home(); + let mut config = MultiAppConfig::default(); { let manager = config @@ -447,9 +442,7 @@ fn provider_service_switch_codex_missing_auth_returns_error() { ); } - let state = AppState { - config: RwLock::new(config), - }; + let state = create_test_state_with_config(&config).expect("create test state"); let err = ProviderService::switch(&state, AppType::Codex, "invalid") .expect_err("switching should fail without auth"); @@ -508,17 +501,15 @@ fn provider_service_delete_codex_removes_provider_and_files() { std::fs::write(&auth_path, "{}").expect("seed auth file"); std::fs::write(&cfg_path, "base_url = \"https://example\"").expect("seed config file"); - let app_state = AppState { - config: RwLock::new(config), - }; + let app_state = create_test_state_with_config(&config).expect("create test state"); ProviderService::delete(&app_state, AppType::Codex, "to-delete") .expect("delete provider should succeed"); - let locked = app_state.config.read().expect("lock config after delete"); - let manager = locked.get_manager(&AppType::Codex).expect("codex manager"); + let providers = app_state.db.get_all_providers(AppType::Codex.as_str()) + .expect("get all providers"); assert!( - !manager.providers.contains_key("to-delete"), + !providers.contains_key("to-delete"), "provider entry should be removed" ); assert!( @@ -571,18 +562,14 @@ fn provider_service_delete_claude_removes_provider_files() { std::fs::write(&by_name, "{}").expect("seed settings by name"); std::fs::write(&by_id, "{}").expect("seed settings by id"); - let app_state = AppState { - config: RwLock::new(config), - }; + let app_state = create_test_state_with_config(&config).expect("create test state"); ProviderService::delete(&app_state, AppType::Claude, "delete").expect("delete claude provider"); - let locked = app_state.config.read().expect("lock config after delete"); - let manager = locked - .get_manager(&AppType::Claude) - .expect("claude manager"); + let providers = app_state.db.get_all_providers(AppType::Claude.as_str()) + .expect("get all providers"); assert!( - !manager.providers.contains_key("delete"), + !providers.contains_key("delete"), "claude provider should be removed" ); assert!( @@ -612,9 +599,7 @@ fn provider_service_delete_current_provider_returns_error() { ); } - let app_state = AppState { - config: RwLock::new(config), - }; + let app_state = create_test_state_with_config(&config).expect("create test state"); let err = ProviderService::delete(&app_state, AppType::Claude, "keep") .expect_err("deleting current provider should fail"); diff --git a/src-tauri/tests/support.rs b/src-tauri/tests/support.rs index d8d27896c..b954bad79 100644 --- a/src-tauri/tests/support.rs +++ b/src-tauri/tests/support.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; -use std::sync::{Mutex, OnceLock}; +use std::sync::{Arc, Mutex, OnceLock}; -use cc_switch_lib::{update_settings, AppSettings}; +use cc_switch_lib::{update_settings, AppSettings, AppState, Database, MultiAppConfig}; /// 为测试设置隔离的 HOME 目录,避免污染真实用户数据。 pub fn ensure_test_home() -> &'static Path { @@ -45,3 +45,18 @@ pub fn test_mutex() -> &'static Mutex<()> { static MUTEX: OnceLock> = OnceLock::new(); MUTEX.get_or_init(|| Mutex::new(())) } + +/// 创建测试用的 AppState,包含一个空的数据库 +pub fn create_test_state() -> Result> { + let db = Database::init()?; + Ok(AppState { db: Arc::new(db) }) +} + +/// 创建测试用的 AppState,并从 MultiAppConfig 迁移数据 +pub fn create_test_state_with_config( + config: &MultiAppConfig, +) -> Result> { + let db = Database::init()?; + db.migrate_from_json(config)?; + Ok(AppState { db: Arc::new(db) }) +}