From 6a9aa7aeb5c775eb71371b24e45cccc15c7df036 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 27 Oct 2025 22:30:57 +0800 Subject: [PATCH] refactor(backend): phase 3 - add integration tests for config sync (partial) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add integration test suite with isolated test environment: - New test file: tests/import_export_sync.rs (149 lines, 3 test cases) * sync_claude_provider_writes_live_settings: validates SSOT sync to live settings * create_backup_skips_missing_file: edge case handling for missing config * create_backup_generates_snapshot_file: verifies backup snapshot creation - Test infrastructure: * OnceLock-based isolated HOME directory in temp folder * Mutex guard to ensure sequential test execution (avoid file system race) * Automatic cleanup between test runs Export core APIs for testing (lib.rs): - AppType, MultiAppConfig, Provider (data structures) - get_claude_settings_path, read_json_file (config utilities) - create_backup, sync_current_providers_to_live (sync operations) - update_settings, AppSettings (settings management) Adjust visibility: - import_export::sync_current_providers_to_live: fn -> pub fn Update documentation: - Mark Phase 3 as in-progress (🚧) in BACKEND_REFACTOR_PLAN.md - Document current test coverage scope and pending scenarios Test results: 7/7 passed (4 unit + 3 integration) Build time: 0.16s Next steps: - Add Codex sync tests (auth.json + config.toml atomic writes) - Add MCP sync integration tests - Add end-to-end provider switching tests --- docs/BACKEND_REFACTOR_PLAN.md | 4 + src-tauri/src/import_export.rs | 2 +- src-tauri/src/lib.rs | 6 ++ src-tauri/tests/import_export_sync.rs | 148 ++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src-tauri/tests/import_export_sync.rs diff --git a/docs/BACKEND_REFACTOR_PLAN.md b/docs/BACKEND_REFACTOR_PLAN.md index fbe35fe..ac613a0 100644 --- a/docs/BACKEND_REFACTOR_PLAN.md +++ b/docs/BACKEND_REFACTOR_PLAN.md @@ -76,6 +76,10 @@ - 已将单一 `src-tauri/src/commands.rs` 拆分为 `commands/{provider,mcp,config,settings,misc,plugin}.rs` 并通过 `commands/mod.rs` 统一导出,保持对外 API 不变。 - 每个文件聚焦单一功能域(供应商、MCP、配置、设置、杂项、插件),命令函数平均 150-250 行,可读性与后续维护性显著提升。 - 相关依赖调整后 `cargo check` 通过,静态巡检确认无重复定义或未注册命令。 +- **阶段 3:补充测试 🚧** + - 新增 `tests/import_export_sync.rs` 集成测试,覆盖配置备份与 Claude 供应商 live 同步路径(使用隔离的 HOME 目录,避免污染真实环境)。 + - 扩展 `lib.rs` 对核心数据结构与错误处理 API 的导出,便于后续服务层测试复用。 + - 当前覆盖率聚焦配置导入导出模块,后续待补充 MCP 同步、供应商切换等跨模块场景。 ## 渐进式重构路线 diff --git a/src-tauri/src/import_export.rs b/src-tauri/src/import_export.rs index c782d30..f0b58f2 100644 --- a/src-tauri/src/import_export.rs +++ b/src-tauri/src/import_export.rs @@ -81,7 +81,7 @@ fn cleanup_old_backups(backup_dir: &PathBuf, retain: usize) -> Result<(), AppErr Ok(()) } -fn sync_current_providers_to_live(config: &mut MultiAppConfig) -> Result<(), AppError> { +pub fn sync_current_providers_to_live(config: &mut MultiAppConfig) -> Result<(), AppError> { sync_current_provider_for_app(config, &AppType::Claude)?; sync_current_provider_for_app(config, &AppType::Codex)?; Ok(()) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 772ce87..93d7e52 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,6 +15,12 @@ mod speedtest; mod store; mod usage_script; +pub use app_config::{AppType, MultiAppConfig}; +pub use config::{get_claude_settings_path, read_json_file}; +pub use import_export::{create_backup, sync_current_providers_to_live}; +pub use provider::Provider; +pub use settings::{update_settings, AppSettings}; + use store::AppState; use tauri::{ menu::{CheckMenuItem, Menu, MenuBuilder, MenuItem}, diff --git a/src-tauri/tests/import_export_sync.rs b/src-tauri/tests/import_export_sync.rs new file mode 100644 index 0000000..4ca40ff --- /dev/null +++ b/src-tauri/tests/import_export_sync.rs @@ -0,0 +1,148 @@ +use std::fs; +use std::path::Path; +use serde_json::json; +use std::sync::{Mutex, OnceLock}; + +use cc_switch_lib::{ + create_backup, get_claude_settings_path, read_json_file, sync_current_providers_to_live, + update_settings, AppSettings, AppType, MultiAppConfig, Provider, +}; + +fn ensure_test_home() -> &'static Path { + static HOME: OnceLock = OnceLock::new(); + HOME.get_or_init(|| { + let base = std::env::temp_dir().join("cc-switch-test-home"); + if base.exists() { + let _ = std::fs::remove_dir_all(&base); + } + std::fs::create_dir_all(&base).expect("create test home"); + std::env::set_var("HOME", &base); + #[cfg(windows)] + std::env::set_var("USERPROFILE", &base); + base + }) + .as_path() +} + +fn reset_test_fs() { + let home = ensure_test_home(); + for sub in [".claude", ".codex", ".cc-switch"] { + let path = home.join(sub); + if path.exists() { + if let Err(err) = fs::remove_dir_all(&path) { + eprintln!("failed to clean {}: {}", path.display(), err); + } + } + } + // 重置内存中的设置缓存,确保测试环境不受上一次调用影响 + // 写入默认设置即可刷新 OnceLock 中的缓存数据 + let _ = update_settings(AppSettings::default()); +} + +fn test_mutex() -> &'static Mutex<()> { + static MUTEX: OnceLock> = OnceLock::new(); + MUTEX.get_or_init(|| Mutex::new(())) +} + +#[test] +fn sync_claude_provider_writes_live_settings() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + + let mut config = MultiAppConfig::default(); + let provider_config = json!({ + "env": { + "ANTHROPIC_AUTH_TOKEN": "test-key", + "ANTHROPIC_BASE_URL": "https://api.test" + }, + "ui": { + "displayName": "Test Provider" + } + }); + + let provider = Provider::with_id( + "prov-1".to_string(), + "Test Claude".to_string(), + provider_config.clone(), + None, + ); + + let manager = config + .get_manager_mut(&AppType::Claude) + .expect("claude manager"); + manager.providers.insert("prov-1".to_string(), provider); + manager.current = "prov-1".to_string(); + + sync_current_providers_to_live(&mut config).expect("sync live settings"); + + let settings_path = get_claude_settings_path(); + assert!( + settings_path.exists(), + "live settings should be written to {}", + settings_path.display() + ); + + let live_value: serde_json::Value = read_json_file(&settings_path).expect("read live file"); + assert_eq!(live_value, provider_config); + + // 确认 SSOT 中的供应商也同步了最新内容 + let updated = config + .get_manager(&AppType::Claude) + .and_then(|m| m.providers.get("prov-1")) + .expect("provider in config"); + assert_eq!(updated.settings_config, provider_config); + + // 额外确认写入位置位于测试 HOME 下 + assert!( + settings_path.starts_with(home), + "settings path {:?} should reside under test HOME {:?}", + settings_path, + home + ); +} + +#[test] +fn create_backup_skips_missing_file() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + let config_path = home.join(".cc-switch").join("config.json"); + + // 未创建文件时应返回空字符串,不报错 + let result = create_backup(&config_path).expect("create backup"); + assert!( + result.is_empty(), + "expected empty backup id when config file missing" + ); +} + +#[test] +fn create_backup_generates_snapshot_file() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + let config_dir = home.join(".cc-switch"); + let config_path = config_dir.join("config.json"); + fs::create_dir_all(&config_dir).expect("prepare config dir"); + fs::write(&config_path, r#"{"version":2}"#).expect("write config file"); + + let backup_id = create_backup(&config_path).expect("backup success"); + assert!( + !backup_id.is_empty(), + "backup id should contain timestamp information" + ); + + let backup_path = config_dir.join("backups").join(format!("{backup_id}.json")); + assert!( + backup_path.exists(), + "expected backup file at {}", + backup_path.display() + ); + + let backup_content = fs::read_to_string(&backup_path).expect("read backup"); + assert!( + backup_content.contains(r#""version":2"#), + "backup content should match original config" + ); +}