refactor(config): remove v1 auto-migration and improve error handling
BREAKING CHANGE: Runtime auto-migration from v1 to v2 config format has been removed. Changes: - Remove automatic v1→v2 migration logic from MultiAppConfig::load() - Improve v1 detection using structural analysis (checks for 'apps' key absence) - Return clear error with migration instructions when v1 config is detected - Add comprehensive tests for config loading edge cases - Fix false positive detection when v1 config contains 'version' or 'mcp' fields Migration path for users: 1. Install v3.2.x to perform one-time auto-migration, OR 2. Manually edit ~/.cc-switch/config.json to v2 format Rationale: - Separates concerns: load() should be read-only, not perform side effects - Fail-fast principle: unsupported formats should error immediately - Simplifies code maintenance by removing migration logic from hot path Tests added: - load_v1_config_returns_error_and_does_not_write - load_v1_with_extra_version_still_treated_as_v1 - load_invalid_json_returns_parse_error_and_does_not_write - load_valid_v2_config_succeeds
This commit is contained in:
105
src-tauri/tests/app_config_load.rs
Normal file
105
src-tauri/tests/app_config_load.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cc_switch_lib::{AppError, MultiAppConfig};
|
||||
|
||||
mod support;
|
||||
use support::{ensure_test_home, reset_test_fs, test_mutex};
|
||||
|
||||
fn cfg_path() -> PathBuf {
|
||||
let home = std::env::var("HOME").expect("HOME should be set by ensure_test_home");
|
||||
PathBuf::from(home).join(".cc-switch").join("config.json")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_v1_config_returns_error_and_does_not_write() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
let path = cfg_path();
|
||||
fs::create_dir_all(path.parent().unwrap()).expect("create cfg dir");
|
||||
|
||||
// 最小 v1 形状:providers + current,且不含 version/apps/mcp
|
||||
let v1_json = r#"{"providers":{},"current":""}"#;
|
||||
fs::write(&path, v1_json).expect("seed v1 json");
|
||||
let before = fs::read_to_string(&path).expect("read before");
|
||||
|
||||
let err = MultiAppConfig::load().expect_err("v1 should not be auto-migrated");
|
||||
match err {
|
||||
AppError::Localized { key, .. } => assert_eq!(key, "config.unsupported_v1"),
|
||||
other => panic!("expected Localized v1 error, got {other:?}"),
|
||||
}
|
||||
|
||||
// 文件不应有任何变化,且不应生成 .bak
|
||||
let after = fs::read_to_string(&path).expect("read after");
|
||||
assert_eq!(before, after, "config.json should not be modified");
|
||||
let bak = home.join(".cc-switch").join("config.json.bak");
|
||||
assert!(!bak.exists(), ".bak should not be created on load error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_v1_with_extra_version_still_treated_as_v1() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
let path = cfg_path();
|
||||
std::fs::create_dir_all(path.parent().unwrap()).expect("create cfg dir");
|
||||
|
||||
// 畸形:包含 providers + current + version,但没有 apps,应按 v1 处理
|
||||
let v1_like = r#"{"providers":{},"current":"","version":2}"#;
|
||||
std::fs::write(&path, v1_like).expect("seed v1-like json");
|
||||
let before = std::fs::read_to_string(&path).expect("read before");
|
||||
|
||||
let err = MultiAppConfig::load().expect_err("v1-like should not be parsed as v2");
|
||||
match err {
|
||||
AppError::Localized { key, .. } => assert_eq!(key, "config.unsupported_v1"),
|
||||
other => panic!("expected Localized v1 error, got {other:?}"),
|
||||
}
|
||||
|
||||
let after = std::fs::read_to_string(&path).expect("read after");
|
||||
assert_eq!(before, after, "config.json should not be modified");
|
||||
let bak = home.join(".cc-switch").join("config.json.bak");
|
||||
assert!(!bak.exists(), ".bak should not be created on v1-like error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_invalid_json_returns_parse_error_and_does_not_write() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
let path = cfg_path();
|
||||
fs::create_dir_all(path.parent().unwrap()).expect("create cfg dir");
|
||||
|
||||
fs::write(&path, "{not json").expect("seed invalid json");
|
||||
let before = fs::read_to_string(&path).expect("read before");
|
||||
|
||||
let err = MultiAppConfig::load().expect_err("invalid json should error");
|
||||
match err {
|
||||
AppError::Json { .. } => {}
|
||||
other => panic!("expected Json error, got {other:?}"),
|
||||
}
|
||||
|
||||
let after = fs::read_to_string(&path).expect("read after");
|
||||
assert_eq!(before, after, "config.json should remain unchanged");
|
||||
let bak = home.join(".cc-switch").join("config.json.bak");
|
||||
assert!(!bak.exists(), ".bak should not be created on parse error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_valid_v2_config_succeeds() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
let path = cfg_path();
|
||||
fs::create_dir_all(path.parent().unwrap()).expect("create cfg dir");
|
||||
|
||||
// 使用默认结构序列化为 v2
|
||||
let default_cfg = MultiAppConfig::default();
|
||||
let json = serde_json::to_string_pretty(&default_cfg).expect("serialize default cfg");
|
||||
fs::write(&path, json).expect("write v2 json");
|
||||
|
||||
let loaded = MultiAppConfig::load().expect("v2 should load successfully");
|
||||
assert_eq!(loaded.version, 2);
|
||||
assert!(loaded.get_manager(&cc_switch_lib::AppType::Claude).is_some());
|
||||
assert!(loaded.get_manager(&cc_switch_lib::AppType::Codex).is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user