Files
cc-switch/src-tauri/tests/app_config_load.rs
Jason db80e96786 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
2025-11-06 09:18:21 +08:00

106 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());
}