refactor(backend): phase 4 - add test hooks and extend service layer
- 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
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use serde_json::json;
|
||||
use std::{fs, path::Path, sync::Mutex};
|
||||
use tauri::async_runtime;
|
||||
use serde_json::json;
|
||||
|
||||
use cc_switch_lib::{
|
||||
create_backup, get_claude_settings_path, import_config_from_path, read_json_file,
|
||||
@@ -77,22 +77,18 @@ fn sync_codex_provider_writes_auth_and_config() {
|
||||
let mut config = MultiAppConfig::default();
|
||||
|
||||
// 添加入测 MCP 启用项,确保 sync_enabled_to_codex 会写入 TOML
|
||||
config
|
||||
.mcp
|
||||
.codex
|
||||
.servers
|
||||
.insert(
|
||||
"echo-server".into(),
|
||||
json!({
|
||||
"id": "echo-server",
|
||||
"enabled": true,
|
||||
"server": {
|
||||
"type": "stdio",
|
||||
"command": "echo",
|
||||
"args": ["hello"]
|
||||
}
|
||||
}),
|
||||
);
|
||||
config.mcp.codex.servers.insert(
|
||||
"echo-server".into(),
|
||||
json!({
|
||||
"id": "echo-server",
|
||||
"enabled": true,
|
||||
"server": {
|
||||
"type": "stdio",
|
||||
"command": "echo",
|
||||
"args": ["hello"]
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let provider_config = json!({
|
||||
"auth": {
|
||||
@@ -143,9 +139,7 @@ fn sync_codex_provider_writes_auth_and_config() {
|
||||
);
|
||||
|
||||
// 当前供应商应同步最新 config 文本
|
||||
let manager = config
|
||||
.get_manager(&AppType::Codex)
|
||||
.expect("codex manager");
|
||||
let manager = config.get_manager(&AppType::Codex).expect("codex manager");
|
||||
let synced = manager.providers.get("codex-1").expect("codex provider");
|
||||
let synced_cfg = synced
|
||||
.settings_config
|
||||
@@ -161,22 +155,18 @@ fn sync_enabled_to_codex_writes_enabled_servers() {
|
||||
reset_test_fs();
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config
|
||||
.mcp
|
||||
.codex
|
||||
.servers
|
||||
.insert(
|
||||
"stdio-enabled".into(),
|
||||
json!({
|
||||
"id": "stdio-enabled",
|
||||
"enabled": true,
|
||||
"server": {
|
||||
"type": "stdio",
|
||||
"command": "echo",
|
||||
"args": ["ok"],
|
||||
}
|
||||
}),
|
||||
);
|
||||
config.mcp.codex.servers.insert(
|
||||
"stdio-enabled".into(),
|
||||
json!({
|
||||
"id": "stdio-enabled",
|
||||
"enabled": true,
|
||||
"server": {
|
||||
"type": "stdio",
|
||||
"command": "echo",
|
||||
"args": ["ok"],
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
cc_switch_lib::sync_enabled_to_codex(&config).expect("sync codex");
|
||||
|
||||
@@ -241,10 +231,16 @@ fn sync_enabled_to_codex_returns_error_on_invalid_toml() {
|
||||
let err = cc_switch_lib::sync_enabled_to_codex(&config).expect_err("sync should fail");
|
||||
match err {
|
||||
cc_switch_lib::AppError::Toml { path, .. } => {
|
||||
assert!(path.ends_with("config.toml"), "path should reference config.toml");
|
||||
assert!(
|
||||
path.ends_with("config.toml"),
|
||||
"path should reference config.toml"
|
||||
);
|
||||
}
|
||||
cc_switch_lib::AppError::McpValidation(msg) => {
|
||||
assert!(msg.contains("config.toml"), "error message should mention config.toml");
|
||||
assert!(
|
||||
msg.contains("config.toml"),
|
||||
"error message should mention config.toml"
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected error: {other:?}"),
|
||||
}
|
||||
@@ -400,16 +396,31 @@ url = "https://example.com"
|
||||
assert!(changed >= 2, "should import both servers");
|
||||
|
||||
let servers = &config.mcp.codex.servers;
|
||||
let echo = servers.get("echo_server").and_then(|v| v.as_object()).expect("echo server");
|
||||
let echo = servers
|
||||
.get("echo_server")
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("echo server");
|
||||
assert_eq!(echo.get("enabled").and_then(|v| v.as_bool()), Some(true));
|
||||
let server_spec = echo.get("server").and_then(|v| v.as_object()).expect("server spec");
|
||||
let server_spec = echo
|
||||
.get("server")
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("server spec");
|
||||
assert_eq!(
|
||||
server_spec.get("command").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
server_spec
|
||||
.get("command")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(""),
|
||||
"echo"
|
||||
);
|
||||
|
||||
let http = servers.get("http_server").and_then(|v| v.as_object()).expect("http server");
|
||||
let http_spec = http.get("server").and_then(|v| v.as_object()).expect("http spec");
|
||||
let http = servers
|
||||
.get("http_server")
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("http server");
|
||||
let http_spec = http
|
||||
.get("server")
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("http spec");
|
||||
assert_eq!(
|
||||
http_spec.get("url").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
"https://example.com"
|
||||
@@ -434,22 +445,18 @@ command = "echo"
|
||||
.expect("write codex config");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config
|
||||
.mcp
|
||||
.codex
|
||||
.servers
|
||||
.insert(
|
||||
"existing".into(),
|
||||
json!({
|
||||
"id": "existing",
|
||||
"name": "existing",
|
||||
"enabled": false,
|
||||
"server": {
|
||||
"type": "stdio",
|
||||
"command": "prev"
|
||||
}
|
||||
}),
|
||||
);
|
||||
config.mcp.codex.servers.insert(
|
||||
"existing".into(),
|
||||
json!({
|
||||
"id": "existing",
|
||||
"name": "existing",
|
||||
"enabled": false,
|
||||
"server": {
|
||||
"type": "stdio",
|
||||
"command": "prev"
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let changed = cc_switch_lib::import_from_codex(&mut config).expect("import codex");
|
||||
assert!(changed >= 1, "should mark change for enabled flag");
|
||||
@@ -462,7 +469,10 @@ command = "echo"
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("existing entry");
|
||||
assert_eq!(entry.get("enabled").and_then(|v| v.as_bool()), Some(true));
|
||||
let spec = entry.get("server").and_then(|v| v.as_object()).expect("server spec");
|
||||
let spec = entry
|
||||
.get("server")
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("server spec");
|
||||
// 保留原 command,确保导入不会覆盖现有 server 细节
|
||||
assert_eq!(spec.get("command").and_then(|v| v.as_str()), Some("prev"));
|
||||
}
|
||||
@@ -542,11 +552,9 @@ fn import_from_claude_merges_into_config() {
|
||||
.expect("write claude json");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config
|
||||
.mcp
|
||||
.claude
|
||||
.servers
|
||||
.insert("stdio-enabled".into(), json!({
|
||||
config.mcp.claude.servers.insert(
|
||||
"stdio-enabled".into(),
|
||||
json!({
|
||||
"id": "stdio-enabled",
|
||||
"name": "stdio-enabled",
|
||||
"enabled": false,
|
||||
@@ -554,7 +562,8 @@ fn import_from_claude_merges_into_config() {
|
||||
"type": "stdio",
|
||||
"command": "prev"
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
let changed = cc_switch_lib::import_from_claude(&mut config).expect("import from claude");
|
||||
assert!(changed >= 1, "should mark at least one change");
|
||||
@@ -567,7 +576,10 @@ fn import_from_claude_merges_into_config() {
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("entry exists");
|
||||
assert_eq!(entry.get("enabled").and_then(|v| v.as_bool()), Some(true));
|
||||
let server = entry.get("server").and_then(|v| v.as_object()).expect("server obj");
|
||||
let server = entry
|
||||
.get("server")
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("server obj");
|
||||
assert_eq!(
|
||||
server.get("command").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
"prev",
|
||||
@@ -745,10 +757,7 @@ fn import_config_from_path_overwrites_state_and_creates_backup() {
|
||||
"saved config should record new current provider"
|
||||
);
|
||||
|
||||
let guard = app_state
|
||||
.config
|
||||
.lock()
|
||||
.expect("lock state after import");
|
||||
let guard = app_state.config.lock().expect("lock state after import");
|
||||
let claude_manager = guard
|
||||
.get_manager(&AppType::Claude)
|
||||
.expect("claude manager in state");
|
||||
@@ -778,8 +787,7 @@ fn import_config_from_path_invalid_json_returns_error() {
|
||||
config: Mutex::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let err =
|
||||
import_config_from_path(&invalid_path, &app_state).expect_err("import should fail");
|
||||
let err = import_config_from_path(&invalid_path, &app_state).expect_err("import should fail");
|
||||
match err {
|
||||
AppError::Json { .. } => {}
|
||||
other => panic!("expected json error, got {other:?}"),
|
||||
@@ -825,10 +833,7 @@ fn export_config_to_file_writes_target_path() {
|
||||
export_path.to_string_lossy().to_string(),
|
||||
))
|
||||
.expect("export should succeed");
|
||||
assert_eq!(
|
||||
result.get("success").and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(result.get("success").and_then(|v| v.as_bool()), Some(true));
|
||||
|
||||
let exported = fs::read_to_string(&export_path).expect("read exported file");
|
||||
assert!(
|
||||
|
||||
232
src-tauri/tests/mcp_commands.rs
Normal file
232
src-tauri/tests/mcp_commands.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use std::fs;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use cc_switch_lib::{
|
||||
get_claude_mcp_path, get_claude_settings_path, import_default_config_test_hook,
|
||||
import_mcp_from_claude_test_hook, set_mcp_enabled_test_hook, AppError, AppState, AppType,
|
||||
MultiAppConfig,
|
||||
};
|
||||
|
||||
#[path = "support.rs"]
|
||||
mod support;
|
||||
use support::{ensure_test_home, reset_test_fs, test_mutex};
|
||||
|
||||
#[test]
|
||||
fn import_default_config_claude_persists_provider() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let settings_path = get_claude_settings_path();
|
||||
if let Some(parent) = settings_path.parent() {
|
||||
fs::create_dir_all(parent).expect("create claude settings dir");
|
||||
}
|
||||
let settings = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "test-key",
|
||||
"ANTHROPIC_BASE_URL": "https://api.test"
|
||||
}
|
||||
});
|
||||
fs::write(
|
||||
&settings_path,
|
||||
serde_json::to_string_pretty(&settings).expect("serialize settings"),
|
||||
)
|
||||
.expect("seed claude settings.json");
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::Claude);
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(config),
|
||||
};
|
||||
|
||||
import_default_config_test_hook(&state, AppType::Claude)
|
||||
.expect("import default config succeeds");
|
||||
|
||||
// 验证内存状态
|
||||
let guard = state.config.lock().expect("lock config");
|
||||
let manager = guard
|
||||
.get_manager(&AppType::Claude)
|
||||
.expect("claude manager present");
|
||||
assert_eq!(manager.current, "default");
|
||||
let default_provider = manager.providers.get("default").expect("default provider");
|
||||
assert_eq!(
|
||||
default_provider.settings_config, settings,
|
||||
"default provider should capture live settings"
|
||||
);
|
||||
drop(guard);
|
||||
|
||||
// 验证配置已持久化
|
||||
let config_path = home.join(".cc-switch").join("config.json");
|
||||
assert!(
|
||||
config_path.exists(),
|
||||
"importing default config should persist config.json"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_default_config_without_live_file_returns_error() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let err = import_default_config_test_hook(&state, AppType::Claude)
|
||||
.expect_err("missing live file should error");
|
||||
match err {
|
||||
AppError::Message(msg) => assert!(
|
||||
msg.contains("Claude Code 配置文件不存在"),
|
||||
"unexpected error message: {msg}"
|
||||
),
|
||||
other => panic!("unexpected error variant: {other:?}"),
|
||||
}
|
||||
|
||||
let config_path = home.join(".cc-switch").join("config.json");
|
||||
assert!(
|
||||
!config_path.exists(),
|
||||
"failed import should not create config.json"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_mcp_from_claude_creates_config_and_enables_servers() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let mcp_path = get_claude_mcp_path();
|
||||
let claude_json = json!({
|
||||
"mcpServers": {
|
||||
"echo": {
|
||||
"type": "stdio",
|
||||
"command": "echo"
|
||||
}
|
||||
}
|
||||
});
|
||||
fs::write(
|
||||
&mcp_path,
|
||||
serde_json::to_string_pretty(&claude_json).expect("serialize claude mcp"),
|
||||
)
|
||||
.expect("seed ~/.claude.json");
|
||||
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let changed =
|
||||
import_mcp_from_claude_test_hook(&state).expect("import mcp from claude succeeds");
|
||||
assert!(
|
||||
changed > 0,
|
||||
"import should report inserted or normalized entries"
|
||||
);
|
||||
|
||||
let guard = state.config.lock().expect("lock config");
|
||||
let claude_servers = &guard.mcp.claude.servers;
|
||||
let entry = claude_servers
|
||||
.get("echo")
|
||||
.expect("server imported into config.json");
|
||||
assert!(
|
||||
entry
|
||||
.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false),
|
||||
"imported server should be marked enabled"
|
||||
);
|
||||
drop(guard);
|
||||
|
||||
let config_path = home.join(".cc-switch").join("config.json");
|
||||
assert!(
|
||||
config_path.exists(),
|
||||
"state.save should persist config.json when changes detected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_mcp_from_claude_invalid_json_preserves_state() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let mcp_path = get_claude_mcp_path();
|
||||
fs::write(&mcp_path, "{\"mcpServers\":") // 不完整 JSON
|
||||
.expect("seed invalid ~/.claude.json");
|
||||
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(MultiAppConfig::default()),
|
||||
};
|
||||
|
||||
let err =
|
||||
import_mcp_from_claude_test_hook(&state).expect_err("invalid json should bubble up error");
|
||||
match err {
|
||||
AppError::McpValidation(msg) => assert!(
|
||||
msg.contains("解析 ~/.claude.json 失败"),
|
||||
"unexpected error message: {msg}"
|
||||
),
|
||||
other => panic!("unexpected error variant: {other:?}"),
|
||||
}
|
||||
|
||||
let config_path = home.join(".cc-switch").join("config.json");
|
||||
assert!(
|
||||
!config_path.exists(),
|
||||
"failed import should not persist config.json"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_mcp_enabled_for_codex_writes_live_config() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
ensure_test_home();
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
config.ensure_app(&AppType::Codex);
|
||||
config.mcp.codex.servers.insert(
|
||||
"codex-server".into(),
|
||||
json!({
|
||||
"id": "codex-server",
|
||||
"name": "Codex Server",
|
||||
"server": {
|
||||
"type": "stdio",
|
||||
"command": "echo"
|
||||
},
|
||||
"enabled": false
|
||||
}),
|
||||
);
|
||||
|
||||
let state = AppState {
|
||||
config: std::sync::Mutex::new(config),
|
||||
};
|
||||
|
||||
set_mcp_enabled_test_hook(&state, AppType::Codex, "codex-server", true)
|
||||
.expect("set enabled should succeed");
|
||||
|
||||
let guard = state.config.lock().expect("lock config");
|
||||
let entry = guard
|
||||
.mcp
|
||||
.codex
|
||||
.servers
|
||||
.get("codex-server")
|
||||
.expect("codex server exists");
|
||||
assert!(
|
||||
entry
|
||||
.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false),
|
||||
"server should be marked enabled after command"
|
||||
);
|
||||
drop(guard);
|
||||
|
||||
let toml_path = cc_switch_lib::get_codex_config_path();
|
||||
assert!(
|
||||
toml_path.exists(),
|
||||
"enabling server should trigger sync to ~/.codex/config.toml"
|
||||
);
|
||||
let toml_text = fs::read_to_string(&toml_path).expect("read codex config");
|
||||
assert!(
|
||||
toml_text.contains("codex-server"),
|
||||
"codex config should include the enabled server definition"
|
||||
);
|
||||
}
|
||||
@@ -88,17 +88,13 @@ command = "say"
|
||||
"live auth.json should reflect new provider"
|
||||
);
|
||||
|
||||
let config_text =
|
||||
std::fs::read_to_string(get_codex_config_path()).expect("read config.toml");
|
||||
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 locked = app_state.config.lock().expect("lock config after switch");
|
||||
let manager = locked
|
||||
.get_manager(&AppType::Codex)
|
||||
.expect("codex manager after switch");
|
||||
@@ -231,10 +227,7 @@ fn switch_provider_updates_claude_live_and_state() {
|
||||
"live settings.json should reflect new provider auth"
|
||||
);
|
||||
|
||||
let locked = app_state
|
||||
.config
|
||||
.lock()
|
||||
.expect("lock config after switch");
|
||||
let locked = app_state.config.lock().expect("lock config after switch");
|
||||
let manager = locked
|
||||
.get_manager(&AppType::Claude)
|
||||
.expect("claude manager after switch");
|
||||
@@ -265,8 +258,7 @@ fn switch_provider_updates_claude_live_and_state() {
|
||||
|
||||
drop(locked);
|
||||
|
||||
let home_dir =
|
||||
std::env::var("HOME").expect("HOME should be set by ensure_test_home");
|
||||
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");
|
||||
@@ -325,13 +317,8 @@ fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() {
|
||||
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");
|
||||
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"
|
||||
|
||||
413
src-tauri/tests/provider_service.rs
Normal file
413
src-tauri/tests/provider_service.rs
Normal file
@@ -0,0 +1,413 @@
|
||||
use serde_json::json;
|
||||
|
||||
use cc_switch_lib::{
|
||||
get_claude_settings_path, read_json_file, write_codex_live_atomic, AppError, AppType,
|
||||
MultiAppConfig, Provider, ProviderService,
|
||||
};
|
||||
|
||||
#[path = "support.rs"]
|
||||
mod support;
|
||||
use support::{ensure_test_home, reset_test_fs, test_mutex};
|
||||
|
||||
fn sanitize_provider_name(name: &str) -> String {
|
||||
name.chars()
|
||||
.map(|c| match c {
|
||||
'<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' => '-',
|
||||
_ => c,
|
||||
})
|
||||
.collect::<String>()
|
||||
.to_lowercase()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_service_switch_codex_updates_live_and_config() {
|
||||
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"
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
ProviderService::switch(&mut config, AppType::Codex, "new-provider")
|
||||
.expect("switch provider should succeed");
|
||||
|
||||
let auth_value: serde_json::Value =
|
||||
read_json_file(&cc_switch_lib::get_codex_auth_path()).expect("read auth.json");
|
||||
assert_eq!(
|
||||
auth_value.get("OPENAI_API_KEY").and_then(|v| v.as_str()),
|
||||
Some("fresh-key"),
|
||||
"live auth.json should reflect new provider"
|
||||
);
|
||||
|
||||
let config_text =
|
||||
std::fs::read_to_string(cc_switch_lib::get_codex_config_path()).expect("read config.toml");
|
||||
assert!(
|
||||
config_text.contains("mcp_servers.echo-server"),
|
||||
"config.toml should contain synced MCP servers"
|
||||
);
|
||||
|
||||
let manager = config
|
||||
.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 provider_service_switch_claude_updates_live_and_state() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let _home = ensure_test_home();
|
||||
|
||||
let settings_path = 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ProviderService::switch(&mut config, 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 manager = config
|
||||
.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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_service_switch_missing_provider_returns_error() {
|
||||
let mut config = MultiAppConfig::default();
|
||||
|
||||
let err = ProviderService::switch(&mut config, AppType::Claude, "missing")
|
||||
.expect_err("switching missing provider should fail");
|
||||
match err {
|
||||
AppError::ProviderNotFound(id) => assert_eq!(id, "missing"),
|
||||
other => panic!("expected ProviderNotFound, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_service_switch_codex_missing_auth_returns_error() {
|
||||
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 err = ProviderService::switch(&mut config, AppType::Codex, "invalid")
|
||||
.expect_err("switching should fail without auth");
|
||||
match err {
|
||||
AppError::Config(msg) => assert!(
|
||||
msg.contains("auth"),
|
||||
"expected auth related message, got {msg}"
|
||||
),
|
||||
other => panic!("expected config error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_service_delete_codex_removes_provider_and_files() {
|
||||
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.current = "keep".to_string();
|
||||
manager.providers.insert(
|
||||
"keep".to_string(),
|
||||
Provider::with_id(
|
||||
"keep".to_string(),
|
||||
"Keep".to_string(),
|
||||
json!({
|
||||
"auth": {"OPENAI_API_KEY": "keep-key"},
|
||||
"config": ""
|
||||
}),
|
||||
None,
|
||||
),
|
||||
);
|
||||
manager.providers.insert(
|
||||
"to-delete".to_string(),
|
||||
Provider::with_id(
|
||||
"to-delete".to_string(),
|
||||
"DeleteCodex".to_string(),
|
||||
json!({
|
||||
"auth": {"OPENAI_API_KEY": "delete-key"},
|
||||
"config": ""
|
||||
}),
|
||||
None,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let sanitized = sanitize_provider_name("DeleteCodex");
|
||||
let codex_dir = home.join(".codex");
|
||||
std::fs::create_dir_all(&codex_dir).expect("create codex dir");
|
||||
let auth_path = codex_dir.join(format!("auth-{}.json", sanitized));
|
||||
let cfg_path = codex_dir.join(format!("config-{}.toml", sanitized));
|
||||
std::fs::write(&auth_path, "{}").expect("seed auth file");
|
||||
std::fs::write(&cfg_path, "base_url = \"https://example\"").expect("seed config file");
|
||||
|
||||
ProviderService::delete(&mut config, AppType::Codex, "to-delete")
|
||||
.expect("delete provider should succeed");
|
||||
|
||||
let manager = config.get_manager(&AppType::Codex).expect("codex manager");
|
||||
assert!(
|
||||
!manager.providers.contains_key("to-delete"),
|
||||
"provider entry should be removed"
|
||||
);
|
||||
assert!(
|
||||
!auth_path.exists() && !cfg_path.exists(),
|
||||
"provider-specific files should be deleted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_service_delete_claude_removes_provider_files() {
|
||||
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::Claude)
|
||||
.expect("claude manager");
|
||||
manager.current = "keep".to_string();
|
||||
manager.providers.insert(
|
||||
"keep".to_string(),
|
||||
Provider::with_id(
|
||||
"keep".to_string(),
|
||||
"Keep".to_string(),
|
||||
json!({
|
||||
"env": { "ANTHROPIC_API_KEY": "keep-key" }
|
||||
}),
|
||||
None,
|
||||
),
|
||||
);
|
||||
manager.providers.insert(
|
||||
"delete".to_string(),
|
||||
Provider::with_id(
|
||||
"delete".to_string(),
|
||||
"DeleteClaude".to_string(),
|
||||
json!({
|
||||
"env": { "ANTHROPIC_API_KEY": "delete-key" }
|
||||
}),
|
||||
None,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let sanitized = sanitize_provider_name("DeleteClaude");
|
||||
let claude_dir = home.join(".claude");
|
||||
std::fs::create_dir_all(&claude_dir).expect("create claude dir");
|
||||
let by_name = claude_dir.join(format!("settings-{}.json", sanitized));
|
||||
let by_id = claude_dir.join("settings-delete.json");
|
||||
std::fs::write(&by_name, "{}").expect("seed settings by name");
|
||||
std::fs::write(&by_id, "{}").expect("seed settings by id");
|
||||
|
||||
ProviderService::delete(&mut config, AppType::Claude, "delete")
|
||||
.expect("delete claude provider");
|
||||
|
||||
let manager = config
|
||||
.get_manager(&AppType::Claude)
|
||||
.expect("claude manager");
|
||||
assert!(
|
||||
!manager.providers.contains_key("delete"),
|
||||
"claude provider should be removed"
|
||||
);
|
||||
assert!(
|
||||
!by_name.exists() && !by_id.exists(),
|
||||
"provider config files should be deleted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_service_delete_current_provider_returns_error() {
|
||||
let mut config = MultiAppConfig::default();
|
||||
{
|
||||
let manager = config
|
||||
.get_manager_mut(&AppType::Claude)
|
||||
.expect("claude manager");
|
||||
manager.current = "keep".to_string();
|
||||
manager.providers.insert(
|
||||
"keep".to_string(),
|
||||
Provider::with_id(
|
||||
"keep".to_string(),
|
||||
"Keep".to_string(),
|
||||
json!({
|
||||
"env": { "ANTHROPIC_API_KEY": "keep-key" }
|
||||
}),
|
||||
None,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let err = ProviderService::delete(&mut config, AppType::Claude, "keep")
|
||||
.expect_err("deleting current provider should fail");
|
||||
match err {
|
||||
AppError::Config(msg) => assert!(
|
||||
msg.contains("不能删除当前正在使用的供应商"),
|
||||
"unexpected message: {msg}"
|
||||
),
|
||||
other => panic!("expected Config error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user