mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-07 06:07:18 +08:00
Make import existing side-effect free (#2429)
Co-authored-by: Xvvln <3369759202@qq.com>
This commit is contained in:
@@ -268,8 +268,8 @@ impl McpService {
|
||||
state.db.save_mcp_server(&to_save)?;
|
||||
existing.insert(to_save.id.clone(), to_save.clone());
|
||||
|
||||
// 同步到对应应用 live 配置
|
||||
Self::sync_server_to_apps(state, &to_save)?;
|
||||
// 导入是读取已有配置,不应反向写回任何应用的 live 配置。
|
||||
// 显式编辑、启用/禁用或手动同步时再执行写回。
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,8 +306,8 @@ impl McpService {
|
||||
state.db.save_mcp_server(&to_save)?;
|
||||
existing.insert(to_save.id.clone(), to_save.clone());
|
||||
|
||||
// 同步到对应应用 live 配置
|
||||
Self::sync_server_to_apps(state, &to_save)?;
|
||||
// 导入是读取已有配置,不应反向写回任何应用的 live 配置。
|
||||
// 显式编辑、启用/禁用或手动同步时再执行写回。
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,8 +344,8 @@ impl McpService {
|
||||
state.db.save_mcp_server(&to_save)?;
|
||||
existing.insert(to_save.id.clone(), to_save.clone());
|
||||
|
||||
// 同步到对应应用 live 配置
|
||||
Self::sync_server_to_apps(state, &to_save)?;
|
||||
// 导入是读取已有配置,不应反向写回任何应用的 live 配置。
|
||||
// 显式编辑、启用/禁用或手动同步时再执行写回。
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,8 +382,8 @@ impl McpService {
|
||||
state.db.save_mcp_server(&to_save)?;
|
||||
existing.insert(to_save.id.clone(), to_save.clone());
|
||||
|
||||
// 同步到对应应用 live 配置
|
||||
Self::sync_server_to_apps(state, &to_save)?;
|
||||
// 导入是读取已有配置,不应反向写回任何应用的 live 配置。
|
||||
// 显式编辑、启用/禁用或手动同步时再执行写回。
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,8 +420,8 @@ impl McpService {
|
||||
state.db.save_mcp_server(&to_save)?;
|
||||
existing.insert(to_save.id.clone(), to_save.clone());
|
||||
|
||||
// 同步到对应应用 live 配置
|
||||
Self::sync_server_to_apps(state, &to_save)?;
|
||||
// 导入是读取已有配置,不应反向写回任何应用的 live 配置。
|
||||
// 显式编辑、启用/禁用或手动同步时再执行写回。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1536,19 +1536,6 @@ impl SkillService {
|
||||
// 保存到数据库
|
||||
db.save_skill(&skill)?;
|
||||
|
||||
// 同步到已启用的应用目录(创建 symlink 或复制文件)
|
||||
for app in AppType::all() {
|
||||
if skill.apps.is_enabled_for(&app) {
|
||||
if let Err(e) = Self::sync_to_app_dir(&skill.directory, &app) {
|
||||
log::warn!(
|
||||
"导入后同步 Skill '{}' 到 {:?} 失败: {e:#}",
|
||||
skill.directory,
|
||||
app
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imported.push(skill);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,110 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_mcp_from_codex_does_not_rewrite_codex_config() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let codex_dir = home.join(".codex");
|
||||
fs::create_dir_all(&codex_dir).expect("create codex dir");
|
||||
let config_path = codex_dir.join("config.toml");
|
||||
let original = r#"# keep user formatting intact
|
||||
model = "gpt-5"
|
||||
|
||||
[mcp.servers.legacy]
|
||||
type = "stdio"
|
||||
command = "echo"
|
||||
|
||||
[mcp_servers.echo]
|
||||
type = "stdio"
|
||||
command = "echo"
|
||||
"#;
|
||||
fs::write(&config_path, original).expect("seed codex config");
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
let changed = McpService::import_from_codex(&state).expect("import from codex");
|
||||
assert!(changed > 0, "should import servers from Codex config");
|
||||
|
||||
let after = fs::read_to_string(&config_path).expect("read codex config");
|
||||
assert_eq!(
|
||||
after, original,
|
||||
"importing from Codex should not rewrite ~/.codex/config.toml"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_mcp_from_claude_does_not_sync_existing_codex_enabled_server() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let codex_dir = home.join(".codex");
|
||||
fs::create_dir_all(&codex_dir).expect("create codex dir");
|
||||
let codex_config_path = codex_dir.join("config.toml");
|
||||
let codex_original = r#"[mcp.servers.keep_me]
|
||||
type = "stdio"
|
||||
command = "echo"
|
||||
"#;
|
||||
fs::write(&codex_config_path, codex_original).expect("seed codex config");
|
||||
|
||||
let claude_json = json!({
|
||||
"mcpServers": {
|
||||
"shared": {
|
||||
"type": "stdio",
|
||||
"command": "echo"
|
||||
}
|
||||
}
|
||||
});
|
||||
fs::write(
|
||||
get_claude_mcp_path(),
|
||||
serde_json::to_string_pretty(&claude_json).expect("serialize claude mcp"),
|
||||
)
|
||||
.expect("seed claude mcp");
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
state
|
||||
.db
|
||||
.save_mcp_server(&McpServer {
|
||||
id: "shared".to_string(),
|
||||
name: "shared".to_string(),
|
||||
server: json!({
|
||||
"type": "stdio",
|
||||
"command": "echo"
|
||||
}),
|
||||
apps: McpApps {
|
||||
claude: false,
|
||||
codex: true,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
hermes: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
docs: None,
|
||||
tags: Vec::new(),
|
||||
})
|
||||
.expect("seed existing mcp server");
|
||||
|
||||
let changed = McpService::import_from_claude(&state).expect("import from claude");
|
||||
assert_eq!(changed, 0, "existing server should not count as new");
|
||||
|
||||
let after = fs::read_to_string(&codex_config_path).expect("read codex config");
|
||||
assert_eq!(
|
||||
after, codex_original,
|
||||
"importing from Claude should not sync an existing Codex-enabled server"
|
||||
);
|
||||
|
||||
let servers = state.db.get_all_mcp_servers().expect("get all mcp servers");
|
||||
let shared = servers.get("shared").expect("shared server exists");
|
||||
assert!(
|
||||
shared.apps.claude,
|
||||
"import should enable Claude in database"
|
||||
);
|
||||
assert!(shared.apps.codex, "existing Codex flag should be preserved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_mcp_from_claude_invalid_json_preserves_state() {
|
||||
use support::create_test_state;
|
||||
|
||||
@@ -72,6 +72,53 @@ fn import_from_apps_respects_explicit_app_selection() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_from_apps_does_not_rewrite_selected_app_directory() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let ssot_skill_dir = home.join(".cc-switch").join("skills").join("codex-skill");
|
||||
write_skill(&ssot_skill_dir, "Stale SSOT Skill");
|
||||
fs::write(ssot_skill_dir.join("prompt.md"), "stale ssot").expect("write stale ssot prompt");
|
||||
|
||||
let codex_skill_dir = home.join(".codex").join("skills").join("codex-skill");
|
||||
write_skill(&codex_skill_dir, "Live Codex Skill");
|
||||
fs::write(codex_skill_dir.join("prompt.md"), "live codex").expect("write live codex prompt");
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
|
||||
let imported = SkillService::import_from_apps(
|
||||
&state.db,
|
||||
vec![ImportSkillSelection {
|
||||
directory: "codex-skill".to_string(),
|
||||
apps: SkillApps {
|
||||
codex: true,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
)
|
||||
.expect("import skills");
|
||||
|
||||
assert_eq!(imported.len(), 1, "expected exactly one imported skill");
|
||||
assert!(
|
||||
imported[0].apps.codex,
|
||||
"import should preserve the selected Codex app state"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(codex_skill_dir.join("prompt.md")).expect("read live codex prompt"),
|
||||
"live codex",
|
||||
"import should not replace the app skill directory with SSOT contents"
|
||||
);
|
||||
assert!(
|
||||
!fs::symlink_metadata(&codex_skill_dir)
|
||||
.expect("read codex skill metadata")
|
||||
.file_type()
|
||||
.is_symlink(),
|
||||
"import should not replace the app skill directory with a managed symlink"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_to_app_removes_disabled_and_orphaned_ssot_symlinks() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
|
||||
Reference in New Issue
Block a user