- Extract internal functions in commands/mcp.rs and commands/provider.rs to enable unit testing without Tauri context - Add test hooks: set_mcp_enabled_test_hook, import_mcp_from_claude_test_hook, import_mcp_from_codex_test_hook, import_default_config_test_hook - Migrate error types from String to AppError for precise error matching in tests - Extend ProviderService with delete() method to unify Codex/Claude cleanup logic - Add comprehensive test coverage: - tests/mcp_commands.rs: command-level tests for MCP operations - tests/provider_service.rs: service-level tests for switch/delete operations - Run cargo fmt to fix formatting issues (EOF newlines) - Update BACKEND_REFACTOR_PLAN.md to mark phase 3 complete
327 lines
9.9 KiB
Rust
327 lines
9.9 KiB
Rust
use serde_json::json;
|
|
|
|
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,
|
|
};
|
|
|
|
#[path = "support.rs"]
|
|
mod support;
|
|
use support::{ensure_test_home, reset_test_fs, test_mutex};
|
|
|
|
#[test]
|
|
fn switch_provider_updates_codex_live_and_state() {
|
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
|
reset_test_fs();
|
|
let _home = ensure_test_home();
|
|
|
|
let legacy_auth = json!({"OPENAI_API_KEY": "legacy-key"});
|
|
let legacy_config = r#"[mcp_servers.legacy]
|
|
type = "stdio"
|
|
command = "echo"
|
|
"#;
|
|
write_codex_live_atomic(&legacy_auth, Some(legacy_config))
|
|
.expect("seed existing codex live config");
|
|
|
|
let mut config = MultiAppConfig::default();
|
|
{
|
|
let manager = config
|
|
.get_manager_mut(&AppType::Codex)
|
|
.expect("codex manager");
|
|
manager.current = "old-provider".to_string();
|
|
manager.providers.insert(
|
|
"old-provider".to_string(),
|
|
Provider::with_id(
|
|
"old-provider".to_string(),
|
|
"Legacy".to_string(),
|
|
json!({
|
|
"auth": {"OPENAI_API_KEY": "stale"},
|
|
"config": "stale-config"
|
|
}),
|
|
None,
|
|
),
|
|
);
|
|
manager.providers.insert(
|
|
"new-provider".to_string(),
|
|
Provider::with_id(
|
|
"new-provider".to_string(),
|
|
"Latest".to_string(),
|
|
json!({
|
|
"auth": {"OPENAI_API_KEY": "fresh-key"},
|
|
"config": r#"[mcp_servers.latest]
|
|
type = "stdio"
|
|
command = "say"
|
|
"#
|
|
}),
|
|
None,
|
|
),
|
|
);
|
|
}
|
|
|
|
config.mcp.codex.servers.insert(
|
|
"echo-server".into(),
|
|
json!({
|
|
"id": "echo-server",
|
|
"enabled": true,
|
|
"server": {
|
|
"type": "stdio",
|
|
"command": "echo"
|
|
}
|
|
}),
|
|
);
|
|
|
|
let app_state = AppState {
|
|
config: std::sync::Mutex::new(config),
|
|
};
|
|
|
|
switch_provider_test_hook(&app_state, AppType::Codex, "new-provider")
|
|
.expect("switch provider should succeed");
|
|
|
|
let auth_value: serde_json::Value =
|
|
read_json_file(&get_codex_auth_path()).expect("read auth.json");
|
|
assert_eq!(
|
|
auth_value
|
|
.get("OPENAI_API_KEY")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or(""),
|
|
"fresh-key",
|
|
"live auth.json should reflect new provider"
|
|
);
|
|
|
|
let config_text = std::fs::read_to_string(get_codex_config_path()).expect("read config.toml");
|
|
assert!(
|
|
config_text.contains("mcp_servers.echo-server"),
|
|
"config.toml should contain synced MCP servers"
|
|
);
|
|
|
|
let locked = app_state.config.lock().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 new_provider = manager
|
|
.providers
|
|
.get("new-provider")
|
|
.expect("new provider exists");
|
|
let new_config_text = new_provider
|
|
.settings_config
|
|
.get("config")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or_default();
|
|
assert_eq!(
|
|
new_config_text, config_text,
|
|
"provider config snapshot should match live file"
|
|
);
|
|
|
|
let legacy = manager
|
|
.providers
|
|
.get("old-provider")
|
|
.expect("legacy provider still exists");
|
|
let legacy_auth_value = legacy
|
|
.settings_config
|
|
.get("auth")
|
|
.and_then(|v| v.get("OPENAI_API_KEY"))
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("");
|
|
assert_eq!(
|
|
legacy_auth_value, "legacy-key",
|
|
"previous provider should be backfilled with live auth"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn switch_provider_missing_provider_returns_error() {
|
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
|
reset_test_fs();
|
|
|
|
let mut config = MultiAppConfig::default();
|
|
config
|
|
.get_manager_mut(&AppType::Claude)
|
|
.expect("claude manager")
|
|
.current = "does-not-exist".to_string();
|
|
|
|
let app_state = AppState {
|
|
config: std::sync::Mutex::new(config),
|
|
};
|
|
|
|
let err = switch_provider_test_hook(&app_state, AppType::Claude, "missing-provider")
|
|
.expect_err("switching to a missing provider should fail");
|
|
|
|
assert!(
|
|
err.to_string().contains("供应商不存在"),
|
|
"error message should mention missing provider"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn switch_provider_updates_claude_live_and_state() {
|
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
|
reset_test_fs();
|
|
let _home = ensure_test_home();
|
|
|
|
let settings_path = cc_switch_lib::get_claude_settings_path();
|
|
if let Some(parent) = settings_path.parent() {
|
|
std::fs::create_dir_all(parent).expect("create claude settings dir");
|
|
}
|
|
let legacy_live = json!({
|
|
"env": {
|
|
"ANTHROPIC_API_KEY": "legacy-key"
|
|
},
|
|
"workspace": {
|
|
"path": "/tmp/workspace"
|
|
}
|
|
});
|
|
std::fs::write(
|
|
&settings_path,
|
|
serde_json::to_string_pretty(&legacy_live).expect("serialize legacy live"),
|
|
)
|
|
.expect("seed claude live config");
|
|
|
|
let mut config = MultiAppConfig::default();
|
|
{
|
|
let manager = config
|
|
.get_manager_mut(&AppType::Claude)
|
|
.expect("claude manager");
|
|
manager.current = "old-provider".to_string();
|
|
manager.providers.insert(
|
|
"old-provider".to_string(),
|
|
Provider::with_id(
|
|
"old-provider".to_string(),
|
|
"Legacy Claude".to_string(),
|
|
json!({
|
|
"env": { "ANTHROPIC_API_KEY": "stale-key" }
|
|
}),
|
|
None,
|
|
),
|
|
);
|
|
manager.providers.insert(
|
|
"new-provider".to_string(),
|
|
Provider::with_id(
|
|
"new-provider".to_string(),
|
|
"Fresh Claude".to_string(),
|
|
json!({
|
|
"env": { "ANTHROPIC_API_KEY": "fresh-key" },
|
|
"workspace": { "path": "/tmp/new-workspace" }
|
|
}),
|
|
None,
|
|
),
|
|
);
|
|
}
|
|
|
|
let app_state = AppState {
|
|
config: std::sync::Mutex::new(config),
|
|
};
|
|
|
|
switch_provider_test_hook(&app_state, AppType::Claude, "new-provider")
|
|
.expect("switch provider should succeed");
|
|
|
|
let live_after: serde_json::Value =
|
|
read_json_file(&settings_path).expect("read claude live settings");
|
|
assert_eq!(
|
|
live_after
|
|
.get("env")
|
|
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
|
|
.and_then(|key| key.as_str()),
|
|
Some("fresh-key"),
|
|
"live settings.json should reflect new provider auth"
|
|
);
|
|
|
|
let locked = app_state.config.lock().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 legacy_provider = manager
|
|
.providers
|
|
.get("old-provider")
|
|
.expect("legacy provider still exists");
|
|
assert_eq!(
|
|
legacy_provider.settings_config, legacy_live,
|
|
"previous provider should receive backfilled live config"
|
|
);
|
|
|
|
let new_provider = manager
|
|
.providers
|
|
.get("new-provider")
|
|
.expect("new provider exists");
|
|
assert_eq!(
|
|
new_provider
|
|
.settings_config
|
|
.get("env")
|
|
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
|
|
.and_then(|key| key.as_str()),
|
|
Some("fresh-key"),
|
|
"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")
|
|
.join("config.json");
|
|
assert!(
|
|
config_path.exists(),
|
|
"switching provider should persist config.json"
|
|
);
|
|
let persisted: serde_json::Value =
|
|
serde_json::from_str(&std::fs::read_to_string(&config_path).expect("read saved config"))
|
|
.expect("parse saved config");
|
|
assert_eq!(
|
|
persisted
|
|
.get("claude")
|
|
.and_then(|claude| claude.get("current"))
|
|
.and_then(|current| current.as_str()),
|
|
Some("new-provider"),
|
|
"saved config.json should record the new current provider"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() {
|
|
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
|
|
.get_manager_mut(&AppType::Codex)
|
|
.expect("codex manager");
|
|
manager.providers.insert(
|
|
"invalid".to_string(),
|
|
Provider::with_id(
|
|
"invalid".to_string(),
|
|
"Broken Codex".to_string(),
|
|
json!({
|
|
"config": "[mcp_servers.test]\ncommand = \"noop\""
|
|
}),
|
|
None,
|
|
),
|
|
);
|
|
}
|
|
|
|
let app_state = AppState {
|
|
config: std::sync::Mutex::new(config),
|
|
};
|
|
|
|
let err = switch_provider_test_hook(&app_state, AppType::Codex, "invalid")
|
|
.expect_err("switching should fail when auth missing");
|
|
match err {
|
|
AppError::Config(msg) => assert!(
|
|
msg.contains("auth"),
|
|
"expected auth missing error message, got {msg}"
|
|
),
|
|
other => panic!("expected config error, got {other:?}"),
|
|
}
|
|
|
|
let locked = app_state.config.lock().expect("lock config after failure");
|
|
let manager = locked.get_manager(&AppType::Codex).expect("codex manager");
|
|
assert!(
|
|
manager.current.is_empty(),
|
|
"current provider should remain empty on failure"
|
|
);
|
|
}
|