mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix: align Claude MCP path for custom config dirs (#3431)
* fix claude mcp path for custom config dir * fix claude mcp custom profile isolation * fix claude mcp override path edges * fix ci test isolation
This commit is contained in:
@@ -1339,8 +1339,10 @@ mod tests {
|
||||
}
|
||||
|
||||
fn set_proxy_port(db: &Database, port: u16) {
|
||||
let mut config = crate::proxy::types::ProxyConfig::default();
|
||||
config.listen_port = port;
|
||||
let config = crate::proxy::types::ProxyConfig {
|
||||
listen_port: port,
|
||||
..Default::default()
|
||||
};
|
||||
futures::executor::block_on(db.update_proxy_config(config)).expect("update proxy config");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::config::{atomic_write, get_claude_mcp_path, get_default_claude_mcp_path};
|
||||
use crate::config::{atomic_write, get_claude_mcp_path};
|
||||
use crate::error::AppError;
|
||||
|
||||
/// 需要在 Windows 上用 cmd /c 包装的命令
|
||||
@@ -98,51 +98,9 @@ pub struct McpStatus {
|
||||
}
|
||||
|
||||
fn user_config_path() -> PathBuf {
|
||||
ensure_mcp_override_migrated();
|
||||
get_claude_mcp_path()
|
||||
}
|
||||
|
||||
fn ensure_mcp_override_migrated() {
|
||||
if crate::settings::get_claude_override_dir().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_path = get_claude_mcp_path();
|
||||
if new_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let legacy_path = get_default_claude_mcp_path();
|
||||
if !legacy_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(parent) = new_path.parent() {
|
||||
if let Err(err) = fs::create_dir_all(parent) {
|
||||
log::warn!("创建 MCP 目录失败: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match fs::copy(&legacy_path, &new_path) {
|
||||
Ok(_) => {
|
||||
log::info!(
|
||||
"已根据覆盖目录复制 MCP 配置: {} -> {}",
|
||||
legacy_path.display(),
|
||||
new_path.display()
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"复制 MCP 配置失败: {} -> {}: {}",
|
||||
legacy_path.display(),
|
||||
new_path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_json_value(path: &Path) -> Result<Value, AppError> {
|
||||
if !path.exists() {
|
||||
return Ok(serde_json::json!({}));
|
||||
|
||||
+159
-26
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
@@ -47,25 +47,118 @@ pub fn get_default_claude_mcp_path() -> PathBuf {
|
||||
get_home_dir().join(".claude.json")
|
||||
}
|
||||
|
||||
fn derive_mcp_path_from_override(dir: &Path) -> Option<PathBuf> {
|
||||
let file_name = dir
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().to_string())?
|
||||
.trim()
|
||||
.to_string();
|
||||
if file_name.is_empty() {
|
||||
return None;
|
||||
fn normalize_path_lexically(path: &Path) -> PathBuf {
|
||||
let mut normalized = PathBuf::new();
|
||||
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
if !normalized.pop() {
|
||||
normalized.push(component.as_os_str());
|
||||
}
|
||||
}
|
||||
Component::Normal(part) => normalized.push(part),
|
||||
Component::RootDir | Component::Prefix(_) => normalized.push(component.as_os_str()),
|
||||
}
|
||||
}
|
||||
let parent = dir.parent().unwrap_or_else(|| Path::new(""));
|
||||
Some(parent.join(format!("{file_name}.json")))
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
/// 获取 Claude MCP 配置文件路径,若设置了目录覆盖则与覆盖目录同级
|
||||
fn comparable_path_key(path: &Path) -> String {
|
||||
let mut key = normalize_path_lexically(path).to_string_lossy().to_string();
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
key = key.replace('\\', "/");
|
||||
}
|
||||
|
||||
while key.len() > 1 && key.ends_with('/') {
|
||||
key.pop();
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
key.make_ascii_lowercase();
|
||||
}
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
fn path_eq_lexical(left: &Path, right: &Path) -> bool {
|
||||
comparable_path_key(left) == comparable_path_key(right)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn derive_wsl_default_mcp_path(dir: &Path) -> Option<PathBuf> {
|
||||
use std::path::Prefix;
|
||||
|
||||
let normalized = normalize_path_lexically(dir);
|
||||
let mut components = normalized.components();
|
||||
let prefix = match components.next()? {
|
||||
Component::Prefix(prefix) => prefix,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let server = match prefix.kind() {
|
||||
Prefix::UNC(server, _) | Prefix::VerbatimUNC(server, _) => server.to_string_lossy(),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if !server.eq_ignore_ascii_case("wsl$") && !server.eq_ignore_ascii_case("wsl.localhost") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut parts = Vec::new();
|
||||
for component in components {
|
||||
match component {
|
||||
Component::RootDir | Component::CurDir => {}
|
||||
Component::Normal(part) => parts.push(part.to_string_lossy().to_string()),
|
||||
Component::ParentDir | Component::Prefix(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
let is_wsl_home_default =
|
||||
parts.len() == 3 && parts[0] == "home" && !parts[1].is_empty() && parts[2] == ".claude";
|
||||
let is_wsl_root_default = parts.len() == 2 && parts[0] == "root" && parts[1] == ".claude";
|
||||
|
||||
if is_wsl_home_default || is_wsl_root_default {
|
||||
return normalized
|
||||
.parent()
|
||||
.map(|parent| parent.join(".claude.json"));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn default_mcp_path_for_config_dir(dir: &Path) -> Option<PathBuf> {
|
||||
let default_config_dir = get_home_dir().join(".claude");
|
||||
if path_eq_lexical(dir, &default_config_dir) {
|
||||
return Some(get_default_claude_mcp_path());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Some(path) = derive_wsl_default_mcp_path(dir) {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn derive_mcp_path_from_override(dir: &Path) -> PathBuf {
|
||||
dir.join(".claude.json")
|
||||
}
|
||||
|
||||
/// 获取 Claude MCP 配置文件路径
|
||||
pub fn get_claude_mcp_path() -> PathBuf {
|
||||
if let Some(custom_dir) = crate::settings::get_claude_override_dir() {
|
||||
if let Some(path) = derive_mcp_path_from_override(&custom_dir) {
|
||||
if let Some(path) = default_mcp_path_for_config_dir(&custom_dir) {
|
||||
return path;
|
||||
}
|
||||
return derive_mcp_path_from_override(&custom_dir);
|
||||
}
|
||||
get_default_claude_mcp_path()
|
||||
}
|
||||
@@ -263,33 +356,73 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn derive_mcp_path_from_override_preserves_folder_name() {
|
||||
fn derive_mcp_path_from_override_uses_config_dir_for_custom_path() {
|
||||
let override_dir = PathBuf::from("/tmp/profile/.claude");
|
||||
let derived = derive_mcp_path_from_override(&override_dir)
|
||||
.expect("should derive path for nested dir");
|
||||
assert_eq!(derived, PathBuf::from("/tmp/profile/.claude.json"));
|
||||
let derived = derive_mcp_path_from_override(&override_dir);
|
||||
assert_eq!(derived, PathBuf::from("/tmp/profile/.claude/.claude.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_mcp_path_from_override_handles_non_hidden_folder() {
|
||||
fn derive_mcp_path_from_override_uses_config_dir_for_non_hidden_folder() {
|
||||
let override_dir = PathBuf::from("/data/claude-config");
|
||||
let derived = derive_mcp_path_from_override(&override_dir)
|
||||
.expect("should derive path for standard dir");
|
||||
assert_eq!(derived, PathBuf::from("/data/claude-config.json"));
|
||||
let derived = derive_mcp_path_from_override(&override_dir);
|
||||
assert_eq!(derived, PathBuf::from("/data/claude-config/.claude.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_mcp_path_from_override_supports_relative_rootless_dir() {
|
||||
let override_dir = PathBuf::from("claude");
|
||||
let derived = derive_mcp_path_from_override(&override_dir)
|
||||
.expect("should derive path for single segment");
|
||||
assert_eq!(derived, PathBuf::from("claude.json"));
|
||||
let derived = derive_mcp_path_from_override(&override_dir);
|
||||
assert_eq!(derived, PathBuf::from("claude/.claude.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_mcp_path_from_root_like_dir_returns_none() {
|
||||
fn derive_mcp_path_from_root_like_dir_uses_root_file() {
|
||||
let override_dir = PathBuf::from("/");
|
||||
assert!(derive_mcp_path_from_override(&override_dir).is_none());
|
||||
let derived = derive_mcp_path_from_override(&override_dir);
|
||||
assert_eq!(derived, PathBuf::from("/.claude.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_mcp_path_from_override_preserves_leading_parent_dirs() {
|
||||
let override_dir = PathBuf::from("../../profiles/work/.claude");
|
||||
let derived = derive_mcp_path_from_override(&override_dir);
|
||||
assert_eq!(derived, override_dir.join(".claude.json"));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn wsl_unc_home_default_uses_split_mcp_path() {
|
||||
let override_dir = PathBuf::from(r"\\wsl$\Ubuntu\home\travis\.claude");
|
||||
let derived = default_mcp_path_for_config_dir(&override_dir)
|
||||
.expect("WSL home default should use split MCP path");
|
||||
assert_eq!(
|
||||
derived,
|
||||
PathBuf::from(r"\\wsl$\Ubuntu\home\travis\.claude.json")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn wsl_unc_root_default_uses_split_mcp_path() {
|
||||
let override_dir = PathBuf::from(r"\\wsl.localhost\Ubuntu\root\.claude");
|
||||
let derived = default_mcp_path_for_config_dir(&override_dir)
|
||||
.expect("WSL root default should use split MCP path");
|
||||
assert_eq!(
|
||||
derived,
|
||||
PathBuf::from(r"\\wsl.localhost\Ubuntu\root\.claude.json")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn wsl_unc_custom_dir_uses_nested_mcp_path() {
|
||||
let override_dir = PathBuf::from(r"\\wsl$\Ubuntu\opt\claude\.claude");
|
||||
assert!(default_mcp_path_for_config_dir(&override_dir).is_none());
|
||||
assert_eq!(
|
||||
derive_mcp_path_from_override(&override_dir),
|
||||
PathBuf::from(r"\\wsl$\Ubuntu\opt\claude\.claude\.claude.json")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -493,8 +493,10 @@ mod tests {
|
||||
|
||||
{
|
||||
let conn = crate::database::lock_conn!(db.conn);
|
||||
let date_str = chrono::DateTime::from_timestamp(old_ts, 0)
|
||||
.unwrap()
|
||||
let date_str = Local
|
||||
.timestamp_opt(old_ts, 0)
|
||||
.single()
|
||||
.expect("old timestamp should be a valid local datetime")
|
||||
.format("%Y-%m-%d")
|
||||
.to_string();
|
||||
conn.execute(
|
||||
|
||||
@@ -463,6 +463,7 @@ base_url = "http://localhost:8080"
|
||||
|
||||
db.update_proxy_config(ProxyConfig {
|
||||
live_takeover_active: true,
|
||||
listen_port: 0,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
@@ -491,7 +492,7 @@ base_url = "http://localhost:8080"
|
||||
)
|
||||
.expect("seed taken-over live file");
|
||||
|
||||
state
|
||||
let proxy_info = state
|
||||
.proxy_service
|
||||
.start()
|
||||
.await
|
||||
@@ -544,7 +545,7 @@ base_url = "http://localhost:8080"
|
||||
live.get("env")
|
||||
.and_then(|env| env.get("ANTHROPIC_BASE_URL"))
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("http://127.0.0.1:15721"),
|
||||
Some(format!("http://127.0.0.1:{}", proxy_info.port).as_str()),
|
||||
"proxy base URL should stay intact"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -5444,8 +5444,10 @@ command = "shared-command"
|
||||
)
|
||||
.expect("set common config snippet");
|
||||
|
||||
let mut proxy_config = ProxyConfig::default();
|
||||
proxy_config.listen_port = 0;
|
||||
let proxy_config = ProxyConfig {
|
||||
listen_port: 0,
|
||||
..Default::default()
|
||||
};
|
||||
db.update_proxy_config(proxy_config)
|
||||
.await
|
||||
.expect("set test proxy config");
|
||||
@@ -5582,8 +5584,10 @@ requires_openai_auth = true
|
||||
let db = Arc::new(Database::memory().expect("init db"));
|
||||
let state = crate::store::AppState::new(db.clone());
|
||||
|
||||
let mut proxy_config = ProxyConfig::default();
|
||||
proxy_config.listen_port = 0;
|
||||
let proxy_config = ProxyConfig {
|
||||
listen_port: 0,
|
||||
..Default::default()
|
||||
};
|
||||
db.update_proxy_config(proxy_config)
|
||||
.await
|
||||
.expect("set test proxy config");
|
||||
|
||||
@@ -4,8 +4,9 @@ use std::fs;
|
||||
use serde_json::json;
|
||||
|
||||
use cc_switch_lib::{
|
||||
get_claude_mcp_path, get_claude_settings_path, import_default_config_test_hook, AppError,
|
||||
AppType, McpApps, McpServer, McpService, MultiAppConfig,
|
||||
get_claude_mcp_path, get_claude_mcp_status, get_claude_settings_path,
|
||||
import_default_config_test_hook, read_claude_mcp_config, update_settings, AppError,
|
||||
AppSettings, AppType, McpApps, McpServer, McpService, MultiAppConfig,
|
||||
};
|
||||
|
||||
#[path = "support.rs"]
|
||||
@@ -673,6 +674,274 @@ fn enabling_claude_mcp_skips_when_claude_config_absent() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_default_claude_dir_keeps_default_split_mcp_path() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
let claude_dir = home.join(".claude");
|
||||
fs::create_dir_all(&claude_dir).expect("create explicit default claude dir");
|
||||
|
||||
update_settings(AppSettings {
|
||||
claude_config_dir: Some(claude_dir.to_string_lossy().to_string()),
|
||||
..AppSettings::default()
|
||||
})
|
||||
.expect("set explicit default claude config dir");
|
||||
|
||||
assert_eq!(
|
||||
get_claude_mcp_path(),
|
||||
home.join(".claude.json"),
|
||||
"explicit default Claude dir should keep Claude Code's split MCP path"
|
||||
);
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
McpService::upsert_server(
|
||||
&state,
|
||||
McpServer {
|
||||
id: "claude-default".to_string(),
|
||||
name: "Claude Default".to_string(),
|
||||
server: json!({
|
||||
"type": "stdio",
|
||||
"command": "echo"
|
||||
}),
|
||||
apps: McpApps {
|
||||
claude: true,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
hermes: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
docs: None,
|
||||
tags: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("sync default Claude MCP");
|
||||
|
||||
assert!(
|
||||
home.join(".claude.json").exists(),
|
||||
"default split MCP file should be written at home/.claude.json"
|
||||
);
|
||||
assert!(
|
||||
!claude_dir.join(".claude.json").exists(),
|
||||
"explicit default dir should not use nested .claude/.claude.json"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_claude_dir_writes_mcp_inside_config_dir() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
let custom_dir = home.join("profiles").join(".claude");
|
||||
fs::create_dir_all(&custom_dir).expect("create custom claude dir");
|
||||
|
||||
update_settings(AppSettings {
|
||||
claude_config_dir: Some(custom_dir.to_string_lossy().to_string()),
|
||||
..AppSettings::default()
|
||||
})
|
||||
.expect("set custom claude config dir");
|
||||
|
||||
let expected_mcp_path = custom_dir.join(".claude.json");
|
||||
assert_eq!(
|
||||
get_claude_mcp_path(),
|
||||
expected_mcp_path,
|
||||
"custom Claude dir should keep MCP state inside the config dir"
|
||||
);
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
McpService::upsert_server(
|
||||
&state,
|
||||
McpServer {
|
||||
id: "claude-custom".to_string(),
|
||||
name: "Claude Custom".to_string(),
|
||||
server: json!({
|
||||
"type": "stdio",
|
||||
"command": "echo"
|
||||
}),
|
||||
apps: McpApps {
|
||||
claude: true,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
hermes: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
docs: None,
|
||||
tags: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("sync custom Claude MCP");
|
||||
|
||||
assert!(
|
||||
expected_mcp_path.exists(),
|
||||
"custom Claude MCP file should be written inside custom dir"
|
||||
);
|
||||
assert!(
|
||||
!home.join("profiles").join(".claude.json").exists(),
|
||||
"custom Claude dir should not write sibling .claude.json"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_claude_dir_sync_does_not_copy_default_profile() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
let home_mcp_path = home.join(".claude.json");
|
||||
let default_profile = json!({
|
||||
"hasCompletedOnboarding": true,
|
||||
"projects": {
|
||||
"/home-project": {
|
||||
"hasTrustDialogAccepted": true
|
||||
}
|
||||
},
|
||||
"mcpServers": {
|
||||
"home-only": {
|
||||
"type": "stdio",
|
||||
"command": "home-command"
|
||||
}
|
||||
},
|
||||
"profileSentinel": "home-profile"
|
||||
});
|
||||
let default_profile_text =
|
||||
serde_json::to_string_pretty(&default_profile).expect("serialize default profile");
|
||||
fs::write(&home_mcp_path, &default_profile_text).expect("seed default Claude profile");
|
||||
|
||||
let custom_dir = home.join("profiles").join("work").join(".claude");
|
||||
fs::create_dir_all(&custom_dir).expect("create custom claude dir");
|
||||
update_settings(AppSettings {
|
||||
claude_config_dir: Some(custom_dir.to_string_lossy().to_string()),
|
||||
..AppSettings::default()
|
||||
})
|
||||
.expect("set custom claude config dir");
|
||||
|
||||
let expected_mcp_path = custom_dir.join(".claude.json");
|
||||
assert_eq!(
|
||||
get_claude_mcp_path(),
|
||||
expected_mcp_path,
|
||||
"custom Claude dir should use nested .claude.json"
|
||||
);
|
||||
assert!(
|
||||
!expected_mcp_path.exists(),
|
||||
"custom profile should start without a live MCP file"
|
||||
);
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
McpService::upsert_server(
|
||||
&state,
|
||||
McpServer {
|
||||
id: "custom-only".to_string(),
|
||||
name: "Custom Only".to_string(),
|
||||
server: json!({
|
||||
"type": "stdio",
|
||||
"command": "custom-command"
|
||||
}),
|
||||
apps: McpApps {
|
||||
claude: true,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
hermes: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
docs: None,
|
||||
tags: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("sync custom Claude MCP");
|
||||
|
||||
let text = fs::read_to_string(&expected_mcp_path).expect("read custom Claude MCP");
|
||||
let value: serde_json::Value = serde_json::from_str(&text).expect("parse custom Claude MCP");
|
||||
let servers = value
|
||||
.get("mcpServers")
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("custom profile should contain mcpServers");
|
||||
assert!(
|
||||
servers.contains_key("custom-only"),
|
||||
"custom profile should contain DB-managed Claude server"
|
||||
);
|
||||
assert!(
|
||||
!servers.contains_key("home-only"),
|
||||
"custom profile should not inherit default profile MCP servers"
|
||||
);
|
||||
assert!(
|
||||
value.get("hasCompletedOnboarding").is_none(),
|
||||
"custom profile should not inherit onboarding state"
|
||||
);
|
||||
assert!(
|
||||
value.get("projects").is_none(),
|
||||
"custom profile should not inherit project trust state"
|
||||
);
|
||||
assert!(
|
||||
value.get("profileSentinel").is_none(),
|
||||
"custom profile should not inherit unrelated default profile fields"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(&home_mcp_path).expect("reread default Claude profile"),
|
||||
default_profile_text,
|
||||
"default Claude profile should remain unchanged"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_claude_dir_read_only_mcp_queries_do_not_create_profile() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
let home_mcp_path = home.join(".claude.json");
|
||||
fs::write(
|
||||
&home_mcp_path,
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"mcpServers": {
|
||||
"home-only": {
|
||||
"type": "stdio",
|
||||
"command": "home-command"
|
||||
}
|
||||
},
|
||||
"profileSentinel": "home-profile"
|
||||
}))
|
||||
.expect("serialize default profile"),
|
||||
)
|
||||
.expect("seed default Claude profile");
|
||||
|
||||
let custom_dir = home.join("profiles").join("work").join(".claude");
|
||||
fs::create_dir_all(&custom_dir).expect("create custom claude dir");
|
||||
update_settings(AppSettings {
|
||||
claude_config_dir: Some(custom_dir.to_string_lossy().to_string()),
|
||||
..AppSettings::default()
|
||||
})
|
||||
.expect("set custom claude config dir");
|
||||
|
||||
let expected_mcp_path = custom_dir.join(".claude.json");
|
||||
assert!(
|
||||
!expected_mcp_path.exists(),
|
||||
"custom profile should start without a live MCP file"
|
||||
);
|
||||
|
||||
let status =
|
||||
futures::executor::block_on(get_claude_mcp_status()).expect("get Claude MCP status");
|
||||
assert_eq!(
|
||||
status.user_config_path,
|
||||
expected_mcp_path.to_string_lossy(),
|
||||
"status should report the custom profile MCP path"
|
||||
);
|
||||
assert!(
|
||||
!status.user_config_exists,
|
||||
"status should report missing custom profile MCP file"
|
||||
);
|
||||
let text =
|
||||
futures::executor::block_on(read_claude_mcp_config()).expect("read Claude MCP config");
|
||||
assert_eq!(text, None, "missing custom profile should read as None");
|
||||
assert!(
|
||||
!expected_mcp_path.exists(),
|
||||
"read-only MCP queries should not copy or create the custom profile"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_all_enabled_removes_known_disabled_but_preserves_unknown_live_entries() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
|
||||
@@ -597,6 +597,14 @@ wire_api = "responses"
|
||||
|
||||
let state = create_test_state_with_config(&initial_config).expect("create test state");
|
||||
|
||||
let mut proxy_config = state.db.get_proxy_config().await.expect("get proxy config");
|
||||
proxy_config.listen_port = 0;
|
||||
state
|
||||
.db
|
||||
.update_proxy_config(proxy_config)
|
||||
.await
|
||||
.expect("use ephemeral proxy port");
|
||||
|
||||
ProviderService::switch(&state, AppType::Codex, "deepseek-provider")
|
||||
.expect("switch from official subscription to DeepSeek");
|
||||
|
||||
@@ -623,6 +631,12 @@ wire_api = "responses"
|
||||
.set_takeover_for_app("codex", true)
|
||||
.await
|
||||
.expect("enable Codex takeover");
|
||||
let proxy_status = state
|
||||
.proxy_service
|
||||
.get_status()
|
||||
.await
|
||||
.expect("read proxy status after takeover");
|
||||
let codex_proxy_base_url = format!("http://127.0.0.1:{}/v1", proxy_status.port);
|
||||
|
||||
let auth_after_takeover: serde_json::Value =
|
||||
read_json_file(&cc_switch_lib::get_codex_auth_path()).expect("read auth after takeover");
|
||||
@@ -634,7 +648,7 @@ wire_api = "responses"
|
||||
let config_after_takeover =
|
||||
std::fs::read_to_string(cc_switch_lib::get_codex_config_path()).expect("read config");
|
||||
assert!(
|
||||
config_after_takeover.contains("http://127.0.0.1:15721/v1"),
|
||||
config_after_takeover.contains(&codex_proxy_base_url),
|
||||
"enabling takeover should point Codex config.toml at the local proxy"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -33,6 +33,7 @@ pub fn reset_test_fs() {
|
||||
".gemini",
|
||||
".config",
|
||||
".openclaw",
|
||||
"profiles",
|
||||
] {
|
||||
let path = home.join(sub);
|
||||
if path.exists() {
|
||||
|
||||
@@ -754,7 +754,7 @@
|
||||
"appConfigDirDescription": "Customize the storage location for CC Switch configuration (point to cloud sync folder to enable config sync)",
|
||||
"browsePlaceholderApp": "e.g., C:\\Users\\Administrator\\.cc-switch",
|
||||
"claudeConfigDir": "Claude Code Configuration Directory",
|
||||
"claudeConfigDirDescription": "Override Claude configuration directory (settings.json) and keep claude.json (MCP) alongside it.",
|
||||
"claudeConfigDirDescription": "Override Claude configuration directory (settings.json). Custom directories store .claude.json (MCP) inside the directory; the default ~/.claude directory keeps Claude Code's split ~/.claude.json path.",
|
||||
"codexConfigDir": "Codex Configuration Directory",
|
||||
"codexConfigDirDescription": "Override Codex configuration directory.",
|
||||
"geminiConfigDir": "Gemini Configuration Directory",
|
||||
|
||||
@@ -754,7 +754,7 @@
|
||||
"appConfigDirDescription": "CC Switch の保存場所をカスタマイズします(クラウド同期フォルダを指定すると設定を同期できます)",
|
||||
"browsePlaceholderApp": "例: C:\\\\Users\\\\Administrator\\\\.cc-switch",
|
||||
"claudeConfigDir": "Claude Code 設定ディレクトリ",
|
||||
"claudeConfigDirDescription": "Claude の設定ディレクトリ(settings.json)を上書きし、claude.json(MCP)も同じ場所に置きます。",
|
||||
"claudeConfigDirDescription": "Claude の設定ディレクトリ(settings.json)を上書きします。カスタムディレクトリでは .claude.json(MCP)をその中に保存し、既定の ~/.claude では Claude Code の既定どおり ~/.claude.json を使います。",
|
||||
"codexConfigDir": "Codex 設定ディレクトリ",
|
||||
"codexConfigDirDescription": "Codex の設定ディレクトリを上書きします。",
|
||||
"geminiConfigDir": "Gemini 設定ディレクトリ",
|
||||
|
||||
@@ -754,7 +754,7 @@
|
||||
"appConfigDirDescription": "自訂 CC Switch 的設定儲存位置(指定至雲端同步資料夾即可雲端同步設定)",
|
||||
"browsePlaceholderApp": "例如:C:\\Users\\Administrator\\.cc-switch",
|
||||
"claudeConfigDir": "Claude Code 設定目錄",
|
||||
"claudeConfigDirDescription": "覆寫 Claude 設定目錄 (settings.json),同時會在同級存放 Claude MCP 的 claude.json。",
|
||||
"claudeConfigDirDescription": "覆寫 Claude 設定目錄 (settings.json)。真正自訂目錄會在目錄內存放 Claude MCP 的 .claude.json;預設 ~/.claude 目錄仍使用 Claude 預設的 ~/.claude.json。",
|
||||
"codexConfigDir": "Codex 設定目錄",
|
||||
"codexConfigDirDescription": "覆寫 Codex 設定目錄。",
|
||||
"geminiConfigDir": "Gemini 設定目錄",
|
||||
|
||||
@@ -754,7 +754,7 @@
|
||||
"appConfigDirDescription": "自定义 CC Switch 的配置存储位置(指定到云同步文件夹即可云同步配置)",
|
||||
"browsePlaceholderApp": "例如:C:\\Users\\Administrator\\.cc-switch",
|
||||
"claudeConfigDir": "Claude Code 配置目录",
|
||||
"claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json),同时会在同级存放 Claude MCP 的 claude.json。",
|
||||
"claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json)。真正自定义目录会在目录内存放 Claude MCP 的 .claude.json;默认 ~/.claude 目录仍使用 Claude 默认的 ~/.claude.json。",
|
||||
"codexConfigDir": "Codex 配置目录",
|
||||
"codexConfigDirDescription": "覆盖 Codex 配置目录。",
|
||||
"geminiConfigDir": "Gemini 配置目录",
|
||||
|
||||
Reference in New Issue
Block a user