revert: restore full config overwrite + Common Config Snippet (revert 992dda5c)

Revert the partial key-field merging refactoring introduced in 992dda5c,
along with two dependent commits (24fa8a18, 87604b18) that referenced
the now-removed ClaudeQuickToggles component.

The whitelist-based partial merge approach had critical issues:
- Non-whitelisted custom fields were lost during provider switching
- Backfill permanently stripped non-key fields from the database
- Whitelist required constant maintenance to track upstream changes

This restores the proven "full config overwrite + Common Config Snippet"
architecture where each provider stores its complete configuration and
shared settings are managed via a separate snippet mechanism.

Reverted commits:
- 24fa8a18: context-aware JSON editor hint + hide quick toggles
- 87604b18: hide ClaudeQuickToggles when creating
- 992dda5c: partial key-field merging refactoring

Restored:
- Full config snapshot write (write_live_snapshot) for Claude/Codex/Gemini
- Full config backfill (settings_config = live_config)
- Common Config Snippet UI and backend commands
- 6 frontend components/hooks for common config editing
- configApi barrel export and DB snippet methods

Removed:
- ClaudeQuickToggles component
- write_live_partial / backfill_key_fields / patch_claude_live
- All KEY_FIELDS constants
This commit is contained in:
Jason
2026-02-27 23:12:34 +08:00
parent b2b20dadd7
commit 859f413756
39 changed files with 3285 additions and 857 deletions
+271 -1
View File
@@ -2,7 +2,10 @@ use serde_json::json;
use std::fs;
use std::path::PathBuf;
use cc_switch_lib::{AppError, AppType, ConfigService, MultiAppConfig, Provider};
use cc_switch_lib::{
get_claude_settings_path, read_json_file, AppError, AppType, ConfigService, MultiAppConfig,
Provider, ProviderMeta,
};
#[path = "support.rs"]
mod support;
@@ -10,6 +13,132 @@ use support::{
create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex,
};
#[test]
fn sync_claude_provider_writes_live_settings() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let home = ensure_test_home();
let mut config = MultiAppConfig::default();
let provider_config = json!({
"env": {
"ANTHROPIC_AUTH_TOKEN": "test-key",
"ANTHROPIC_BASE_URL": "https://api.test"
},
"ui": {
"displayName": "Test Provider"
}
});
let provider = Provider::with_id(
"prov-1".to_string(),
"Test Claude".to_string(),
provider_config.clone(),
None,
);
let manager = config
.get_manager_mut(&AppType::Claude)
.expect("claude manager");
manager.providers.insert("prov-1".to_string(), provider);
manager.current = "prov-1".to_string();
ConfigService::sync_current_providers_to_live(&mut config).expect("sync live settings");
let settings_path = get_claude_settings_path();
assert!(
settings_path.exists(),
"live settings should be written to {}",
settings_path.display()
);
let live_value: serde_json::Value = read_json_file(&settings_path).expect("read live file");
assert_eq!(live_value, provider_config);
// 确认 SSOT 中的供应商也同步了最新内容
let updated = config
.get_manager(&AppType::Claude)
.and_then(|m| m.providers.get("prov-1"))
.expect("provider in config");
assert_eq!(updated.settings_config, provider_config);
// 额外确认写入位置位于测试 HOME 下
assert!(
settings_path.starts_with(home),
"settings path {settings_path:?} should reside under test HOME {home:?}"
);
}
#[test]
fn sync_codex_provider_writes_auth_and_config() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let mut config = MultiAppConfig::default();
// 注意:v3.7.0 后 MCP 同步由 McpService 独立处理,不再通过 provider 切换触发
// 此测试仅验证 auth.json 和 config.toml 基础配置的写入
let provider_config = json!({
"auth": {
"OPENAI_API_KEY": "codex-key"
},
"config": r#"base_url = "https://codex.test""#
});
let provider = Provider::with_id(
"codex-1".to_string(),
"Codex Test".to_string(),
provider_config.clone(),
None,
);
let manager = config
.get_manager_mut(&AppType::Codex)
.expect("codex manager");
manager.providers.insert("codex-1".to_string(), provider);
manager.current = "codex-1".to_string();
ConfigService::sync_current_providers_to_live(&mut config).expect("sync codex live");
let auth_path = cc_switch_lib::get_codex_auth_path();
let config_path = cc_switch_lib::get_codex_config_path();
assert!(
auth_path.exists(),
"auth.json should exist at {}",
auth_path.display()
);
assert!(
config_path.exists(),
"config.toml should exist at {}",
config_path.display()
);
let auth_value: serde_json::Value = read_json_file(&auth_path).expect("read auth");
assert_eq!(
auth_value,
provider_config.get("auth").cloned().expect("auth object")
);
let toml_text = fs::read_to_string(&config_path).expect("read config.toml");
// 验证基础配置正确写入
assert!(
toml_text.contains("base_url"),
"config.toml should contain base_url from provider config"
);
// 当前供应商应同步最新 config 文本
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
.get("config")
.and_then(|v| v.as_str())
.expect("config string");
assert_eq!(synced_cfg, toml_text);
}
#[test]
fn sync_enabled_to_codex_writes_enabled_servers() {
let _guard = test_mutex().lock().expect("acquire test mutex");
@@ -209,6 +338,46 @@ fn sync_enabled_to_codex_returns_error_on_invalid_toml() {
}
}
#[test]
fn sync_codex_provider_missing_auth_returns_error() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let mut config = MultiAppConfig::default();
let provider = Provider::with_id(
"codex-missing-auth".to_string(),
"No Auth".to_string(),
json!({
"config": "model = \"test\""
}),
None,
);
let manager = config
.get_manager_mut(&AppType::Codex)
.expect("codex manager");
manager.providers.insert(provider.id.clone(), provider);
manager.current = "codex-missing-auth".to_string();
let err = ConfigService::sync_current_providers_to_live(&mut config)
.expect_err("sync should fail when auth missing");
match err {
cc_switch_lib::AppError::Config(msg) => {
assert!(msg.contains("auth"), "error message should mention auth");
}
other => panic!("unexpected error variant: {other:?}"),
}
// 确认未产生任何 live 配置文件
assert!(
!cc_switch_lib::get_codex_auth_path().exists(),
"auth.json should not be created on failure"
);
assert!(
!cc_switch_lib::get_codex_config_path().exists(),
"config.toml should not be created on failure"
);
}
#[test]
fn write_codex_live_atomic_persists_auth_and_config() {
let _guard = test_mutex().lock().expect("acquire test mutex");
@@ -647,6 +816,107 @@ fn create_backup_retains_only_latest_entries() {
);
}
#[test]
fn sync_gemini_packycode_sets_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-1".to_string();
manager.providers.insert(
"packy-1".to_string(),
Provider::with_id(
"packy-1".to_string(),
"PackyCode".to_string(),
json!({
"env": {
"GEMINI_API_KEY": "pk-key",
"GOOGLE_GEMINI_BASE_URL": "https://api-slb.packyapi.com"
}
}),
Some("https://www.packyapi.com".to_string()),
),
);
}
ConfigService::sync_current_providers_to_live(&mut config)
.expect("syncing gemini live should succeed");
// security field is 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 raw = std::fs::read_to_string(&gemini_settings).expect("read gemini settings.json");
let value: serde_json::Value = serde_json::from_str(&raw).expect("parse gemini settings.json");
assert_eq!(
value
.pointer("/security/auth/selectedType")
.and_then(|v| v.as_str()),
Some("gemini-api-key"),
"syncing PackyCode Gemini should enforce security.auth.selectedType in Gemini settings"
);
}
#[test]
fn sync_gemini_google_official_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);
}
ConfigService::sync_current_providers_to_live(&mut config)
.expect("syncing google official gemini should succeed");
// security field is 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 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 json");
assert_eq!(
gemini_value
.pointer("/security/auth/selectedType")
.and_then(|v| v.as_str()),
Some("oauth-personal"),
"Gemini settings should record oauth-personal for Google Official"
);
}
#[test]
fn export_sql_writes_to_target_path() {
let _guard = test_mutex().lock().expect("acquire test mutex");
+12 -23
View File
@@ -100,12 +100,9 @@ command = "say"
);
let config_text = std::fs::read_to_string(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"
config_text.contains("mcp_servers.echo-server"),
"config.toml should contain synced MCP servers"
);
let current_id = app_state
@@ -129,9 +126,12 @@ command = "say"
.get("config")
.and_then(|v| v.as_str())
.unwrap_or_default();
// With partial merge, only key fields (model_provider, model, model_providers)
// are written to the live file. MCP servers are synced separately.
// The provider's stored config should still contain mcp_servers.latest.
// 供应商配置应该包含在 live 文件中
// 注意:live 文件还会包含 MCP 同步后的内容
assert!(
config_text.contains("mcp_servers.latest"),
"live file should contain provider's original config"
);
assert!(
new_config_text.contains("mcp_servers.latest"),
"provider snapshot should contain provider's original config"
@@ -268,22 +268,11 @@ fn switch_provider_updates_claude_live_and_state() {
let legacy_provider = providers
.get("old-provider")
.expect("legacy provider still exists");
// Backfill mechanism: before switching, the live config's key fields are
// backfilled to the current provider. With partial merge, only key fields
// (auth, model, endpoint) are extracted — non-key fields like workspace
// are NOT included in the backfill.
// 回填机制:切换前会将 live 配置回填到当前供应商
// 这保护了用户在 live 文件中的手动修改
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 be backfilled with live auth key"
);
assert!(
legacy_provider.settings_config.get("workspace").is_none(),
"backfill should NOT include non-key fields like workspace"
legacy_provider.settings_config, legacy_live,
"previous provider should be backfilled with live config"
);
let new_provider = providers.get("new-provider").expect("new provider exists");
+9 -17
View File
@@ -112,12 +112,9 @@ command = "say"
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"
config_text.contains("mcp_servers.echo-server"),
"config.toml should contain synced MCP servers"
);
let current_id = state
@@ -146,6 +143,11 @@ command = "say"
new_config_text.contains("mcp_servers.latest"),
"provider config should contain original MCP servers"
);
// live 文件额外包含同步的 MCP 服务器
assert!(
config_text.contains("mcp_servers.echo-server"),
"live config should include synced MCP servers"
);
let legacy = providers
.get("old-provider")
@@ -412,19 +414,9 @@ fn provider_service_switch_claude_updates_live_and_state() {
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"
legacy_provider.settings_config, legacy_live,
"previous provider should receive backfilled live config"
);
}