use std::ffi::OsString;
use std::fs;
use std::path::PathBuf;
use cc_switch_lib::{get_app_config_dir, update_settings, AppError, AppSettings, MultiAppConfig};
mod support;
use support::{ensure_test_home, lock_test_mutex, reset_test_fs};
fn cfg_path() -> PathBuf {
get_app_config_dir().join("config.json")
}
struct ConfigDirEnvGuard {
tui_original: Option<OsString>,
legacy_original: Option<OsString>,
}
impl ConfigDirEnvGuard {
fn clear() -> Self {
let tui_original = std::env::var_os("CC_SWITCH_TUI_CONFIG_DIR");
let legacy_original = std::env::var_os("CC_SWITCH_CONFIG_DIR");
unsafe {
std::env::remove_var("CC_SWITCH_TUI_CONFIG_DIR");
std::env::remove_var("CC_SWITCH_CONFIG_DIR");
}
Self {
tui_original,
legacy_original,
}
}
fn set_legacy(value: &str) -> Self {
let guard = Self::clear();
unsafe {
std::env::set_var("CC_SWITCH_CONFIG_DIR", value);
}
guard
}
}
impl Drop for ConfigDirEnvGuard {
fn drop(&mut self) {
match self.tui_original.as_ref() {
Some(value) => unsafe { std::env::set_var("CC_SWITCH_TUI_CONFIG_DIR", value) },
None => unsafe { std::env::remove_var("CC_SWITCH_TUI_CONFIG_DIR") },
}
match self.legacy_original.as_ref() {
Some(value) => unsafe { std::env::set_var("CC_SWITCH_CONFIG_DIR", value) },
None => unsafe { std::env::remove_var("CC_SWITCH_CONFIG_DIR") },
}
}
}
#[test]
fn load_v1_config_returns_error_and_does_not_write() {
let _guard = lock_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");
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:?}"),
}
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 = lock_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");
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 = lock_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 = lock_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");
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());
assert!(loaded
.get_manager(&cc_switch_lib::AppType::OpenClaw)
.is_some());
}
#[test]
fn default_config_contains_openclaw_prompt_root_and_manager() {
let config = MultiAppConfig::default();
assert!(config
.get_manager(&cc_switch_lib::AppType::OpenClaw)
.is_some());
assert!(
config.prompts.openclaw.prompts.is_empty(),
"default OpenClaw prompt store should exist"
);
}
#[test]
fn update_settings_persists_openclaw_override_dir() {
let _guard = lock_test_mutex();
reset_test_fs();
let _home = ensure_test_home();
let _config_dir = ConfigDirEnvGuard::clear();
let mut settings = AppSettings::default();
settings.openclaw_config_dir = Some("~/custom-openclaw".to_string());
update_settings(settings).expect("save settings with openclaw override");
let path = get_app_config_dir().join("settings.json");
let raw = fs::read_to_string(&path).expect("read settings.json");
let value: serde_json::Value = serde_json::from_str(&raw).expect("parse settings.json");
assert_eq!(
value
.get("openclawConfigDir")
.and_then(|entry| entry.as_str()),
Some("~/custom-openclaw")
);
}
#[test]
fn update_settings_uses_cc_switch_config_dir_override_for_settings_path() {
let _guard = lock_test_mutex();
reset_test_fs();
let home = ensure_test_home();
let override_dir = home.join("custom-config-root");
let _config_dir = ConfigDirEnvGuard::set_legacy(override_dir.to_string_lossy().as_ref());
let mut settings = AppSettings::default();
settings.openclaw_config_dir = Some("~/custom-openclaw".to_string());
update_settings(settings).expect("save settings with config dir override");
let override_settings = override_dir.join("settings.json");
assert!(
override_settings.exists(),
"settings.json should be written to override dir"
);
let raw = fs::read_to_string(&override_settings).expect("read overridden settings.json");
let value: serde_json::Value = serde_json::from_str(&raw).expect("parse overridden settings");
assert_eq!(
value
.get("openclawConfigDir")
.and_then(|entry| entry.as_str()),
Some("~/custom-openclaw")
);
let default_settings = home.join(".cc-switch-tui").join("settings.json");
assert_ne!(
override_settings, default_settings,
"override path should differ from default path"
);
}