mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
992dda5c5c
* refactor(provider): switch from full config overwrite to partial key-field merging Replace the provider switching mechanism for Claude/Codex/Gemini from full settings_config overwrite to partial key-field replacement, preserving user's non-provider settings (plugins, MCP, permissions, etc.) across switches. - Add write_live_partial() with per-app implementations for Claude (JSON env merge), Codex (auth replace + TOML partial merge), and Gemini (env merge) - Add backfill_key_fields() to extract only provider-specific fields when saving live config back to provider entries - Update switch_normal, sync_current_to_live, add, update to use partial merge - Remove common config snippet feature for Claude/Codex/Gemini (no longer needed with partial merging); preserve OMO common config - Delete 6 frontend files (3 components + 3 hooks), clean up 11 modified files - Remove backend extract_common_config_* methods, 3 Tauri commands, CommonConfigSnippets struct, and related migration code - Update integration tests to validate key-field-only backfill behavior * refactor(cleanup): remove dead code and redundant MCP sync after partial-merge refactor - Remove ConfigService legacy full-overwrite sync methods (~150 lines) - Remove redundant McpService::sync_all_enabled from switch_normal - Switch proxy fallback recovery from write_live_snapshot to write_live_partial - Remove dead ProviderService::write_gemini_live wrapper - Update tests to reflect partial-merge behavior (MCP preserved, not re-synced) * feat(claude): add Quick Toggles for common Claude Code preferences Add checkbox toggles for hideAttribution, alwaysThinking, and enableTeammates that write directly to the live settings file via RFC 7396 JSON Merge Patch. Mirror changes to the form editor using form.watch for reactive updates. * fix(provider): add missing key fields to partial-merge constants Add provider-specific fields verified against official docs to prevent key residue or loss during provider switching: - Claude: CLAUDE_CODE_SUBAGENT_MODEL (env), model (top-level) - Codex: review_model, plan_mode_reasoning_effort - Gemini: GOOGLE_API_KEY (official alternative to GEMINI_API_KEY) * fix(provider): expand partial-merge key fields for Bedrock, Vertex, Foundry and behavior settings Add missing env/top-level fields to CLAUDE_KEY_ENV_FIELDS and CLAUDE_KEY_TOP_LEVEL so that provider switching correctly replaces (and clears) credentials and flags for AWS Bedrock, Google Vertex AI, Microsoft Foundry, and provider behavior overrides like max output tokens and prompt caching. * feat(provider): add auth field selector for Claude providers (AUTH_TOKEN / API_KEY) Allow users to choose between ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY when creating or editing custom Claude providers, persisted in meta.apiKeyField. * refactor(preset): remove AiHubMix hardcoded API_KEY in favor of generic auth selector AiHubMix was the only preset that hardcoded ANTHROPIC_API_KEY before the generic auth field selector was introduced. Now that users can freely choose between AUTH_TOKEN and API_KEY via the UI, remove the special-case and default AiHubMix to the standard ANTHROPIC_AUTH_TOKEN.
660 lines
22 KiB
Rust
660 lines
22 KiB
Rust
use serde_json::json;
|
||
|
||
use cc_switch_lib::{
|
||
get_claude_settings_path, read_json_file, write_codex_live_atomic, AppError, AppType, McpApps,
|
||
McpServer, MultiAppConfig, Provider, ProviderMeta, ProviderService,
|
||
};
|
||
|
||
#[path = "support.rs"]
|
||
mod support;
|
||
use support::{
|
||
create_test_state, create_test_state_with_config, 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 initial_config = MultiAppConfig::default();
|
||
{
|
||
let manager = initial_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,
|
||
),
|
||
);
|
||
}
|
||
|
||
// 使用新的统一 MCP 结构(v3.7.0+)
|
||
let servers = initial_config
|
||
.mcp
|
||
.servers
|
||
.get_or_insert_with(Default::default);
|
||
servers.insert(
|
||
"echo-server".into(),
|
||
McpServer {
|
||
id: "echo-server".into(),
|
||
name: "Echo Server".into(),
|
||
server: json!({
|
||
"type": "stdio",
|
||
"command": "echo"
|
||
}),
|
||
apps: McpApps {
|
||
claude: false,
|
||
codex: true,
|
||
gemini: false,
|
||
opencode: false,
|
||
},
|
||
description: None,
|
||
homepage: None,
|
||
docs: None,
|
||
tags: Vec::new(),
|
||
},
|
||
);
|
||
|
||
let state = create_test_state_with_config(&initial_config).expect("create test state");
|
||
|
||
ProviderService::switch(&state, 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");
|
||
// With partial merge, only key fields (model, provider, model_providers) are
|
||
// merged into config.toml. The existing MCP section should be preserved.
|
||
// MCP sync from DB is handled separately (at startup or explicit sync).
|
||
assert!(
|
||
config_text.contains("mcp_servers.legacy"),
|
||
"config.toml should preserve existing MCP servers after partial merge"
|
||
);
|
||
|
||
let current_id = state
|
||
.db
|
||
.get_current_provider(AppType::Codex.as_str())
|
||
.expect("read current provider after switch");
|
||
assert_eq!(
|
||
current_id.as_deref(),
|
||
Some("new-provider"),
|
||
"current provider updated"
|
||
);
|
||
|
||
let providers = state
|
||
.db
|
||
.get_all_providers(AppType::Codex.as_str())
|
||
.expect("read providers after switch");
|
||
|
||
let new_provider = 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();
|
||
// provider 存储的是原始配置,不包含 MCP 同步后的内容
|
||
assert!(
|
||
new_config_text.contains("mcp_servers.latest"),
|
||
"provider config should contain original MCP servers"
|
||
);
|
||
|
||
let legacy = 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_packycode_gemini_updates_security_selected_type() {
|
||
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::Gemini)
|
||
.expect("gemini manager");
|
||
manager.current = "packy-gemini".to_string();
|
||
manager.providers.insert(
|
||
"packy-gemini".to_string(),
|
||
Provider::with_id(
|
||
"packy-gemini".to_string(),
|
||
"PackyCode".to_string(),
|
||
json!({
|
||
"env": {
|
||
"GEMINI_API_KEY": "pk-key",
|
||
"GOOGLE_GEMINI_BASE_URL": "https://www.packyapi.com"
|
||
}
|
||
}),
|
||
Some("https://www.packyapi.com".to_string()),
|
||
),
|
||
);
|
||
}
|
||
|
||
let state = create_test_state_with_config(&config).expect("create test state");
|
||
|
||
ProviderService::switch(&state, AppType::Gemini, "packy-gemini")
|
||
.expect("switching to PackyCode Gemini should succeed");
|
||
|
||
// Gemini security settings are written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json
|
||
let settings_path = home.join(".gemini").join("settings.json");
|
||
assert!(
|
||
settings_path.exists(),
|
||
"Gemini settings.json should exist at {}",
|
||
settings_path.display()
|
||
);
|
||
let raw = std::fs::read_to_string(&settings_path).expect("read gemini settings.json");
|
||
let value: serde_json::Value =
|
||
serde_json::from_str(&raw).expect("parse gemini settings.json after switch");
|
||
|
||
assert_eq!(
|
||
value
|
||
.pointer("/security/auth/selectedType")
|
||
.and_then(|v| v.as_str()),
|
||
Some("gemini-api-key"),
|
||
"PackyCode Gemini should set security.auth.selectedType"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn packycode_partner_meta_triggers_security_flag_even_without_keywords() {
|
||
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::Gemini)
|
||
.expect("gemini manager");
|
||
manager.current = "packy-meta".to_string();
|
||
let mut provider = Provider::with_id(
|
||
"packy-meta".to_string(),
|
||
"Generic Gemini".to_string(),
|
||
json!({
|
||
"env": {
|
||
"GEMINI_API_KEY": "pk-meta",
|
||
"GOOGLE_GEMINI_BASE_URL": "https://generativelanguage.googleapis.com"
|
||
}
|
||
}),
|
||
Some("https://example.com".to_string()),
|
||
);
|
||
provider.meta = Some(ProviderMeta {
|
||
partner_promotion_key: Some("packycode".to_string()),
|
||
..ProviderMeta::default()
|
||
});
|
||
manager.providers.insert("packy-meta".to_string(), provider);
|
||
}
|
||
|
||
let state = create_test_state_with_config(&config).expect("create test state");
|
||
|
||
ProviderService::switch(&state, AppType::Gemini, "packy-meta")
|
||
.expect("switching to partner meta provider should succeed");
|
||
|
||
// Gemini security settings are written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json
|
||
let settings_path = home.join(".gemini").join("settings.json");
|
||
assert!(
|
||
settings_path.exists(),
|
||
"Gemini settings.json should exist at {}",
|
||
settings_path.display()
|
||
);
|
||
let raw = std::fs::read_to_string(&settings_path).expect("read gemini settings.json");
|
||
let value: serde_json::Value =
|
||
serde_json::from_str(&raw).expect("parse gemini settings.json after switch");
|
||
|
||
assert_eq!(
|
||
value
|
||
.pointer("/security/auth/selectedType")
|
||
.and_then(|v| v.as_str()),
|
||
Some("gemini-api-key"),
|
||
"Partner meta should set security.auth.selectedType even without packy keywords"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn switch_google_official_gemini_sets_oauth_security() {
|
||
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::Gemini)
|
||
.expect("gemini manager");
|
||
manager.current = "google-official".to_string();
|
||
let mut provider = Provider::with_id(
|
||
"google-official".to_string(),
|
||
"Google".to_string(),
|
||
json!({
|
||
"env": {}
|
||
}),
|
||
Some("https://ai.google.dev".to_string()),
|
||
);
|
||
provider.meta = Some(ProviderMeta {
|
||
partner_promotion_key: Some("google-official".to_string()),
|
||
..ProviderMeta::default()
|
||
});
|
||
manager
|
||
.providers
|
||
.insert("google-official".to_string(), provider);
|
||
}
|
||
|
||
let state = create_test_state_with_config(&config).expect("create test state");
|
||
|
||
ProviderService::switch(&state, AppType::Gemini, "google-official")
|
||
.expect("switching to Google official Gemini should succeed");
|
||
|
||
// Gemini security settings are written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json
|
||
let gemini_settings = home.join(".gemini").join("settings.json");
|
||
assert!(
|
||
gemini_settings.exists(),
|
||
"Gemini settings.json should exist at {}",
|
||
gemini_settings.display()
|
||
);
|
||
let gemini_raw = std::fs::read_to_string(&gemini_settings).expect("read gemini settings");
|
||
let gemini_value: serde_json::Value =
|
||
serde_json::from_str(&gemini_raw).expect("parse gemini settings");
|
||
|
||
assert_eq!(
|
||
gemini_value
|
||
.pointer("/security/auth/selectedType")
|
||
.and_then(|v| v.as_str()),
|
||
Some("oauth-personal"),
|
||
"Gemini settings json should reflect oauth-personal for Google Official"
|
||
);
|
||
}
|
||
|
||
#[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,
|
||
),
|
||
);
|
||
}
|
||
|
||
let state = create_test_state_with_config(&config).expect("create test state");
|
||
|
||
ProviderService::switch(&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 providers = state
|
||
.db
|
||
.get_all_providers(AppType::Claude.as_str())
|
||
.expect("get all providers");
|
||
let current_id = state
|
||
.db
|
||
.get_current_provider(AppType::Claude.as_str())
|
||
.expect("get current provider");
|
||
assert_eq!(
|
||
current_id.as_deref(),
|
||
Some("new-provider"),
|
||
"current provider updated"
|
||
);
|
||
|
||
let legacy_provider = providers
|
||
.get("old-provider")
|
||
.expect("legacy provider still exists");
|
||
// With partial merge backfill, only key fields are extracted from live config
|
||
assert_eq!(
|
||
legacy_provider
|
||
.settings_config
|
||
.get("env")
|
||
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
|
||
.and_then(|key| key.as_str()),
|
||
Some("legacy-key"),
|
||
"previous provider should receive backfilled auth key"
|
||
);
|
||
assert!(
|
||
legacy_provider.settings_config.get("workspace").is_none(),
|
||
"backfill should NOT include non-key fields like workspace"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn provider_service_switch_missing_provider_returns_error() {
|
||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||
reset_test_fs();
|
||
let _home = ensure_test_home();
|
||
|
||
let state = create_test_state().expect("create test state");
|
||
|
||
let err = ProviderService::switch(&state, AppType::Claude, "missing")
|
||
.expect_err("switching missing provider should fail");
|
||
match err {
|
||
AppError::Message(msg) => {
|
||
assert!(
|
||
msg.contains("不存在") || msg.contains("not found"),
|
||
"expected provider not found message, got {msg}"
|
||
);
|
||
}
|
||
other => panic!("expected Message error for provider not found, got {other:?}"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn provider_service_switch_codex_missing_auth_returns_error() {
|
||
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 state = create_test_state_with_config(&config).expect("create test state");
|
||
|
||
let err = ProviderService::switch(&state, 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-{sanitized}.json"));
|
||
let cfg_path = codex_dir.join(format!("config-{sanitized}.toml"));
|
||
std::fs::write(&auth_path, "{}").expect("seed auth file");
|
||
std::fs::write(&cfg_path, "base_url = \"https://example\"").expect("seed config file");
|
||
|
||
let app_state = create_test_state_with_config(&config).expect("create test state");
|
||
|
||
ProviderService::delete(&app_state, AppType::Codex, "to-delete")
|
||
.expect("delete provider should succeed");
|
||
|
||
let providers = app_state
|
||
.db
|
||
.get_all_providers(AppType::Codex.as_str())
|
||
.expect("get all providers");
|
||
assert!(
|
||
!providers.contains_key("to-delete"),
|
||
"provider entry should be removed"
|
||
);
|
||
// v3.7.0+ 不再使用供应商特定文件(如 auth-*.json, config-*.toml)
|
||
// 删除供应商只影响数据库记录,不清理这些旧格式文件
|
||
}
|
||
|
||
#[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-{sanitized}.json"));
|
||
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");
|
||
|
||
let app_state = create_test_state_with_config(&config).expect("create test state");
|
||
|
||
ProviderService::delete(&app_state, AppType::Claude, "delete").expect("delete claude provider");
|
||
|
||
let providers = app_state
|
||
.db
|
||
.get_all_providers(AppType::Claude.as_str())
|
||
.expect("get all providers");
|
||
assert!(
|
||
!providers.contains_key("delete"),
|
||
"claude provider should be removed"
|
||
);
|
||
// v3.7.0+ 不再使用供应商特定文件(如 settings-*.json)
|
||
// 删除供应商只影响数据库记录,不清理这些旧格式文件
|
||
}
|
||
|
||
#[test]
|
||
fn provider_service_delete_current_provider_returns_error() {
|
||
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,
|
||
),
|
||
);
|
||
}
|
||
|
||
let app_state = create_test_state_with_config(&config).expect("create test state");
|
||
|
||
let err = ProviderService::delete(&app_state, AppType::Claude, "keep")
|
||
.expect_err("deleting current provider should fail");
|
||
match err {
|
||
AppError::Localized { zh, .. } => assert!(
|
||
zh.contains("不能删除当前正在使用的供应商")
|
||
|| zh.contains("无法删除当前正在使用的供应商"),
|
||
"unexpected message: {zh}"
|
||
),
|
||
AppError::Config(msg) => assert!(
|
||
msg.contains("不能删除当前正在使用的供应商")
|
||
|| msg.contains("无法删除当前正在使用的供应商"),
|
||
"unexpected message: {msg}"
|
||
),
|
||
AppError::Message(msg) => assert!(
|
||
msg.contains("不能删除当前正在使用的供应商")
|
||
|| msg.contains("无法删除当前正在使用的供应商"),
|
||
"unexpected message: {msg}"
|
||
),
|
||
other => panic!("expected Config/Message error, got {other:?}"),
|
||
}
|
||
}
|