mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix(claude): sync takeover live config during provider changes (#1828)
Keep Claude's live settings aligned with the latest provider state while proxy takeover is active, without breaking takeover fields or restore behavior. Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -52,13 +52,56 @@ pub struct SwitchResult {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::config::{get_claude_settings_path, read_json_file, write_json_file};
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::provider::ProviderMeta;
|
use crate::provider::ProviderMeta;
|
||||||
|
use crate::proxy::types::ProxyConfig;
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use serial_test::serial;
|
||||||
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
struct TempHome {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
dir: TempDir,
|
||||||
|
original_home: Option<String>,
|
||||||
|
original_userprofile: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempHome {
|
||||||
|
fn new() -> Self {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp home");
|
||||||
|
let original_home = env::var("HOME").ok();
|
||||||
|
let original_userprofile = env::var("USERPROFILE").ok();
|
||||||
|
|
||||||
|
env::set_var("HOME", dir.path());
|
||||||
|
env::set_var("USERPROFILE", dir.path());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
dir,
|
||||||
|
original_home,
|
||||||
|
original_userprofile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempHome {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
match &self.original_home {
|
||||||
|
Some(value) => env::set_var("HOME", value),
|
||||||
|
None => env::remove_var("HOME"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match &self.original_userprofile {
|
||||||
|
Some(value) => env::set_var("USERPROFILE", value),
|
||||||
|
None => env::remove_var("USERPROFILE"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn test_guard() -> std::sync::MutexGuard<'static, ()> {
|
fn test_guard() -> std::sync::MutexGuard<'static, ()> {
|
||||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||||
@@ -267,6 +310,125 @@ base_url = "http://localhost:8080"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn update_current_claude_provider_syncs_live_when_proxy_takeover_detected_without_backup() {
|
||||||
|
let _home = TempHome::new();
|
||||||
|
crate::settings::reload_settings().expect("reload settings");
|
||||||
|
|
||||||
|
let db = Arc::new(Database::memory().expect("init db"));
|
||||||
|
let state = AppState::new(db.clone());
|
||||||
|
|
||||||
|
let original = Provider::with_id(
|
||||||
|
"p1".into(),
|
||||||
|
"Claude A".into(),
|
||||||
|
json!({
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_API_KEY": "token-a",
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.a.example",
|
||||||
|
"ANTHROPIC_MODEL": "model-a"
|
||||||
|
},
|
||||||
|
"permissions": { "allow": ["Bash"] }
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
db.save_provider("claude", &original)
|
||||||
|
.expect("save provider");
|
||||||
|
db.set_current_provider("claude", "p1")
|
||||||
|
.expect("set current provider");
|
||||||
|
crate::settings::set_current_provider(&AppType::Claude, Some("p1"))
|
||||||
|
.expect("set local current provider");
|
||||||
|
|
||||||
|
db.update_proxy_config(ProxyConfig {
|
||||||
|
live_takeover_active: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("update proxy config");
|
||||||
|
{
|
||||||
|
let mut config = db
|
||||||
|
.get_proxy_config_for_app("claude")
|
||||||
|
.await
|
||||||
|
.expect("get app proxy config");
|
||||||
|
config.enabled = true;
|
||||||
|
db.update_proxy_config_for_app(config)
|
||||||
|
.await
|
||||||
|
.expect("update app proxy config");
|
||||||
|
}
|
||||||
|
|
||||||
|
write_json_file(
|
||||||
|
&get_claude_settings_path(),
|
||||||
|
&json!({
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721",
|
||||||
|
"ANTHROPIC_API_KEY": "PROXY_MANAGED",
|
||||||
|
"ANTHROPIC_MODEL": "stale-model"
|
||||||
|
},
|
||||||
|
"permissions": { "allow": ["Bash"] }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect("seed taken-over live file");
|
||||||
|
|
||||||
|
state.proxy_service.start().await.expect("start proxy service");
|
||||||
|
|
||||||
|
let updated = Provider::with_id(
|
||||||
|
"p1".into(),
|
||||||
|
"Claude A".into(),
|
||||||
|
json!({
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_API_KEY": "token-updated",
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.updated.example",
|
||||||
|
"ANTHROPIC_MODEL": "model-updated"
|
||||||
|
},
|
||||||
|
"permissions": { "allow": ["Read"] }
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
ProviderService::update(&state, AppType::Claude, None, updated.clone())
|
||||||
|
.expect("update current provider");
|
||||||
|
|
||||||
|
let backup = db
|
||||||
|
.get_live_backup("claude")
|
||||||
|
.await
|
||||||
|
.expect("get live backup")
|
||||||
|
.expect("backup exists");
|
||||||
|
let stored_provider = db
|
||||||
|
.get_provider_by_id("p1", "claude")
|
||||||
|
.expect("get stored provider")
|
||||||
|
.expect("stored provider exists");
|
||||||
|
let expected_backup =
|
||||||
|
serde_json::to_string(&stored_provider.settings_config).expect("serialize");
|
||||||
|
assert_eq!(backup.original_config, expected_backup);
|
||||||
|
|
||||||
|
let live: Value = read_json_file(&get_claude_settings_path()).expect("read live");
|
||||||
|
assert_eq!(
|
||||||
|
live.get("permissions"),
|
||||||
|
updated.settings_config.get("permissions"),
|
||||||
|
"provider edits should propagate into Claude live config during takeover"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
live.get("env")
|
||||||
|
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
|
||||||
|
.and_then(|v| v.as_str()),
|
||||||
|
Some("PROXY_MANAGED"),
|
||||||
|
"takeover placeholder should stay intact"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
live.get("env")
|
||||||
|
.and_then(|env| env.get("ANTHROPIC_BASE_URL"))
|
||||||
|
.and_then(|v| v.as_str()),
|
||||||
|
Some("http://127.0.0.1:15721"),
|
||||||
|
"proxy base URL should stay intact"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
live.get("env")
|
||||||
|
.and_then(|env| env.get("ANTHROPIC_MODEL"))
|
||||||
|
.is_none(),
|
||||||
|
"model override should be removed in takeover live config"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rename_rejects_missing_original_provider() {
|
fn rename_rejects_missing_original_provider() {
|
||||||
with_test_home(|state, _| {
|
with_test_home(|state, _| {
|
||||||
@@ -1007,24 +1169,37 @@ impl ProviderService {
|
|||||||
let is_current = effective_current.as_deref() == Some(provider.id.as_str());
|
let is_current = effective_current.as_deref() == Some(provider.id.as_str());
|
||||||
|
|
||||||
if is_current {
|
if is_current {
|
||||||
// 如果代理接管模式处于激活状态,并且代理服务正在运行:
|
// 如果 Claude 代理接管处于激活状态,并且代理服务正在运行:
|
||||||
// - 不写 Live 配置(否则会破坏接管)
|
// - 不直接走普通 Live 写入逻辑
|
||||||
// - 仅更新 Live 备份(保证关闭代理时能恢复到最新配置)
|
// - 改为更新 Live 备份,并在 Claude 下同步代理安全的 Live 配置
|
||||||
let is_app_taken_over =
|
let has_live_backup =
|
||||||
futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
|
futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.is_some();
|
.is_some();
|
||||||
let is_proxy_running = futures::executor::block_on(state.proxy_service.is_running());
|
let is_proxy_running = futures::executor::block_on(state.proxy_service.is_running());
|
||||||
let should_skip_live_write = is_app_taken_over && is_proxy_running;
|
let live_taken_over = state
|
||||||
|
.proxy_service
|
||||||
|
.detect_takeover_in_live_config_for_app(&app_type);
|
||||||
|
let should_sync_via_proxy =
|
||||||
|
is_proxy_running && (has_live_backup || live_taken_over);
|
||||||
|
|
||||||
if should_skip_live_write {
|
if should_sync_via_proxy {
|
||||||
futures::executor::block_on(
|
futures::executor::block_on(
|
||||||
state
|
state
|
||||||
.proxy_service
|
.proxy_service
|
||||||
.update_live_backup_from_provider(app_type.as_str(), &provider),
|
.update_live_backup_from_provider(app_type.as_str(), &provider),
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
||||||
|
|
||||||
|
if matches!(app_type, AppType::Claude) {
|
||||||
|
futures::executor::block_on(
|
||||||
|
state
|
||||||
|
.proxy_service
|
||||||
|
.sync_claude_live_from_provider_while_proxy_active(&provider),
|
||||||
|
)
|
||||||
|
.map_err(|e| AppError::Message(format!("同步 Claude Live 配置失败: {e}")))?;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?;
|
write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?;
|
||||||
// Sync MCP
|
// Sync MCP
|
||||||
|
|||||||
+166
-105
@@ -83,6 +83,65 @@ impl ProxyService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_claude_takeover_fields(config: &mut Value, proxy_url: &str) {
|
||||||
|
if !config.is_object() {
|
||||||
|
*config = json!({});
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = config
|
||||||
|
.as_object_mut()
|
||||||
|
.expect("Claude config should be normalized to an object");
|
||||||
|
let env = root.entry("env".to_string()).or_insert_with(|| json!({}));
|
||||||
|
if !env.is_object() {
|
||||||
|
*env = json!({});
|
||||||
|
}
|
||||||
|
|
||||||
|
let env = env
|
||||||
|
.as_object_mut()
|
||||||
|
.expect("Claude env should be normalized to an object");
|
||||||
|
env.insert("ANTHROPIC_BASE_URL".to_string(), json!(proxy_url));
|
||||||
|
|
||||||
|
for key in CLAUDE_MODEL_OVERRIDE_ENV_KEYS {
|
||||||
|
env.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_keys = [
|
||||||
|
"ANTHROPIC_AUTH_TOKEN",
|
||||||
|
"ANTHROPIC_API_KEY",
|
||||||
|
"OPENROUTER_API_KEY",
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut replaced_any = false;
|
||||||
|
for key in token_keys {
|
||||||
|
if env.contains_key(key) {
|
||||||
|
env.insert(key.to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
||||||
|
replaced_any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !replaced_any {
|
||||||
|
env.insert(
|
||||||
|
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||||
|
json!(PROXY_TOKEN_PLACEHOLDER),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sync_claude_live_from_provider_while_proxy_active(
|
||||||
|
&self,
|
||||||
|
provider: &Provider,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut effective_settings =
|
||||||
|
build_effective_settings_with_common_config(self.db.as_ref(), &AppType::Claude, provider)
|
||||||
|
.map_err(|e| format!("构建 claude 有效配置失败: {e}"))?;
|
||||||
|
let (proxy_url, _) = self.build_proxy_urls().await?;
|
||||||
|
|
||||||
|
Self::apply_claude_takeover_fields(&mut effective_settings, &proxy_url);
|
||||||
|
self.write_claude_live(&effective_settings)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 设置 AppHandle(在应用初始化时调用)
|
/// 设置 AppHandle(在应用初始化时调用)
|
||||||
pub fn set_app_handle(&self, handle: tauri::AppHandle) {
|
pub fn set_app_handle(&self, handle: tauri::AppHandle) {
|
||||||
futures::executor::block_on(async {
|
futures::executor::block_on(async {
|
||||||
@@ -849,41 +908,7 @@ impl ProxyService {
|
|||||||
|
|
||||||
// Claude: 修改 ANTHROPIC_BASE_URL,使用占位符替代真实 Token(代理会注入真实 Token)
|
// Claude: 修改 ANTHROPIC_BASE_URL,使用占位符替代真实 Token(代理会注入真实 Token)
|
||||||
if let Ok(mut live_config) = self.read_claude_live() {
|
if let Ok(mut live_config) = self.read_claude_live() {
|
||||||
if let Some(env) = live_config.get_mut("env").and_then(|v| v.as_object_mut()) {
|
Self::apply_claude_takeover_fields(&mut live_config, &proxy_url);
|
||||||
env.insert("ANTHROPIC_BASE_URL".to_string(), json!(&proxy_url));
|
|
||||||
// 关键:接管模式下移除模型覆盖字段,避免切换供应商后仍用旧模型名发起请求
|
|
||||||
for key in CLAUDE_MODEL_OVERRIDE_ENV_KEYS {
|
|
||||||
env.remove(key);
|
|
||||||
}
|
|
||||||
// 仅覆盖已存在的 Token 字段,避免新增字段导致用户困惑;
|
|
||||||
// 若完全没有 Token 字段,则写入 ANTHROPIC_AUTH_TOKEN 占位符用于避免客户端警告。
|
|
||||||
let token_keys = [
|
|
||||||
"ANTHROPIC_AUTH_TOKEN",
|
|
||||||
"ANTHROPIC_API_KEY",
|
|
||||||
"OPENROUTER_API_KEY",
|
|
||||||
"OPENAI_API_KEY",
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut replaced_any = false;
|
|
||||||
for key in token_keys {
|
|
||||||
if env.contains_key(key) {
|
|
||||||
env.insert(key.to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
|
||||||
replaced_any = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !replaced_any {
|
|
||||||
env.insert(
|
|
||||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
|
||||||
json!(PROXY_TOKEN_PLACEHOLDER),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
live_config["env"] = json!({
|
|
||||||
"ANTHROPIC_BASE_URL": &proxy_url,
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": PROXY_TOKEN_PLACEHOLDER
|
|
||||||
});
|
|
||||||
}
|
|
||||||
self.write_claude_live(&live_config)?;
|
self.write_claude_live(&live_config)?;
|
||||||
log::info!("Claude Live 配置已接管,代理地址: {proxy_url}");
|
log::info!("Claude Live 配置已接管,代理地址: {proxy_url}");
|
||||||
}
|
}
|
||||||
@@ -933,41 +958,7 @@ impl ProxyService {
|
|||||||
match app_type {
|
match app_type {
|
||||||
AppType::Claude => {
|
AppType::Claude => {
|
||||||
let mut live_config = self.read_claude_live()?;
|
let mut live_config = self.read_claude_live()?;
|
||||||
if let Some(env) = live_config.get_mut("env").and_then(|v| v.as_object_mut()) {
|
Self::apply_claude_takeover_fields(&mut live_config, &proxy_url);
|
||||||
env.insert("ANTHROPIC_BASE_URL".to_string(), json!(&proxy_url));
|
|
||||||
// 关键:接管模式下移除模型覆盖字段,避免切换供应商后仍用旧模型名发起请求
|
|
||||||
for key in CLAUDE_MODEL_OVERRIDE_ENV_KEYS {
|
|
||||||
env.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let token_keys = [
|
|
||||||
"ANTHROPIC_AUTH_TOKEN",
|
|
||||||
"ANTHROPIC_API_KEY",
|
|
||||||
"OPENROUTER_API_KEY",
|
|
||||||
"OPENAI_API_KEY",
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut replaced_any = false;
|
|
||||||
for key in token_keys {
|
|
||||||
if env.contains_key(key) {
|
|
||||||
env.insert(key.to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
|
||||||
replaced_any = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !replaced_any {
|
|
||||||
env.insert(
|
|
||||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
|
||||||
json!(PROXY_TOKEN_PLACEHOLDER),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
live_config["env"] = json!({
|
|
||||||
"ANTHROPIC_BASE_URL": &proxy_url,
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": PROXY_TOKEN_PLACEHOLDER
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.write_claude_live(&live_config)?;
|
self.write_claude_live(&live_config)?;
|
||||||
log::info!("Claude Live 配置已接管,代理地址: {proxy_url}");
|
log::info!("Claude Live 配置已接管,代理地址: {proxy_url}");
|
||||||
}
|
}
|
||||||
@@ -1024,41 +1015,7 @@ impl ProxyService {
|
|||||||
match app_type {
|
match app_type {
|
||||||
AppType::Claude => {
|
AppType::Claude => {
|
||||||
if let Ok(mut live_config) = self.read_claude_live() {
|
if let Ok(mut live_config) = self.read_claude_live() {
|
||||||
if let Some(env) = live_config.get_mut("env").and_then(|v| v.as_object_mut()) {
|
Self::apply_claude_takeover_fields(&mut live_config, &proxy_url);
|
||||||
env.insert("ANTHROPIC_BASE_URL".to_string(), json!(&proxy_url));
|
|
||||||
// 关键:接管模式下移除模型覆盖字段,避免切换供应商后仍用旧模型名发起请求
|
|
||||||
for key in CLAUDE_MODEL_OVERRIDE_ENV_KEYS {
|
|
||||||
env.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let token_keys = [
|
|
||||||
"ANTHROPIC_AUTH_TOKEN",
|
|
||||||
"ANTHROPIC_API_KEY",
|
|
||||||
"OPENROUTER_API_KEY",
|
|
||||||
"OPENAI_API_KEY",
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut replaced_any = false;
|
|
||||||
for key in token_keys {
|
|
||||||
if env.contains_key(key) {
|
|
||||||
env.insert(key.to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
|
||||||
replaced_any = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !replaced_any {
|
|
||||||
env.insert(
|
|
||||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
|
||||||
json!(PROXY_TOKEN_PLACEHOLDER),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
live_config["env"] = json!({
|
|
||||||
"ANTHROPIC_BASE_URL": &proxy_url,
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": PROXY_TOKEN_PLACEHOLDER
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = self.write_claude_live(&live_config);
|
let _ = self.write_claude_live(&live_config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1614,6 +1571,8 @@ impl ProxyService {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if matches!(app_type_enum, AppType::Claude) {
|
if matches!(app_type_enum, AppType::Claude) {
|
||||||
|
self.sync_claude_live_from_provider_while_proxy_active(&provider)
|
||||||
|
.await?;
|
||||||
if let Err(e) = self.cleanup_claude_model_overrides_in_live() {
|
if let Err(e) = self.cleanup_claude_model_overrides_in_live() {
|
||||||
log::warn!("清理 Claude Live 模型字段失败(不影响热切换结果): {e}");
|
log::warn!("清理 Claude Live 模型字段失败(不影响热切换结果): {e}");
|
||||||
}
|
}
|
||||||
@@ -2255,6 +2214,108 @@ model = "gpt-5.1-codex"
|
|||||||
assert_eq!(backup.original_config, expected);
|
assert_eq!(backup.original_config, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn hot_switch_provider_updates_claude_live_while_preserving_takeover_fields() {
|
||||||
|
let _home = TempHome::new();
|
||||||
|
crate::settings::reload_settings().expect("reload settings");
|
||||||
|
|
||||||
|
let db = Arc::new(Database::memory().expect("init db"));
|
||||||
|
let service = ProxyService::new(db.clone());
|
||||||
|
|
||||||
|
let provider_a = Provider::with_id(
|
||||||
|
"a".to_string(),
|
||||||
|
"A".to_string(),
|
||||||
|
json!({
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_API_KEY": "a-key",
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.a.example",
|
||||||
|
"ANTHROPIC_MODEL": "claude-old"
|
||||||
|
},
|
||||||
|
"permissions": { "allow": ["Bash"] }
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let provider_b = Provider::with_id(
|
||||||
|
"b".to_string(),
|
||||||
|
"B".to_string(),
|
||||||
|
json!({
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_API_KEY": "b-key",
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.b.example",
|
||||||
|
"ANTHROPIC_MODEL": "claude-new"
|
||||||
|
},
|
||||||
|
"permissions": { "allow": ["Read"] }
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
db.save_provider("claude", &provider_a)
|
||||||
|
.expect("save provider a");
|
||||||
|
db.save_provider("claude", &provider_b)
|
||||||
|
.expect("save provider b");
|
||||||
|
db.set_current_provider("claude", "a")
|
||||||
|
.expect("set current provider");
|
||||||
|
crate::settings::set_current_provider(&AppType::Claude, Some("a"))
|
||||||
|
.expect("set local current provider");
|
||||||
|
db.save_live_backup(
|
||||||
|
"claude",
|
||||||
|
&serde_json::to_string(&provider_a.settings_config).expect("serialize provider a"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("seed live backup");
|
||||||
|
service
|
||||||
|
.write_claude_live(&json!({
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721",
|
||||||
|
"ANTHROPIC_API_KEY": PROXY_TOKEN_PLACEHOLDER,
|
||||||
|
"ANTHROPIC_MODEL": "stale-model"
|
||||||
|
},
|
||||||
|
"permissions": { "allow": ["Bash"] }
|
||||||
|
}))
|
||||||
|
.expect("seed taken-over live file");
|
||||||
|
|
||||||
|
service
|
||||||
|
.hot_switch_provider("claude", "b")
|
||||||
|
.await
|
||||||
|
.expect("hot switch provider");
|
||||||
|
|
||||||
|
let live = service.read_claude_live().expect("read live config");
|
||||||
|
assert_eq!(
|
||||||
|
live.get("permissions"),
|
||||||
|
provider_b.settings_config.get("permissions"),
|
||||||
|
"provider-derived live settings should be refreshed"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
live.get("env")
|
||||||
|
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
|
||||||
|
.and_then(|v| v.as_str()),
|
||||||
|
Some(PROXY_TOKEN_PLACEHOLDER),
|
||||||
|
"takeover token placeholder should be preserved"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
live.get("env")
|
||||||
|
.and_then(|env| env.get("ANTHROPIC_BASE_URL"))
|
||||||
|
.and_then(|v| v.as_str()),
|
||||||
|
Some("http://127.0.0.1:15721"),
|
||||||
|
"takeover proxy URL should remain active"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
live.get("env")
|
||||||
|
.and_then(|env| env.get("ANTHROPIC_MODEL"))
|
||||||
|
.is_none(),
|
||||||
|
"Claude model override fields should be removed in takeover mode"
|
||||||
|
);
|
||||||
|
|
||||||
|
let backup = db
|
||||||
|
.get_live_backup("claude")
|
||||||
|
.await
|
||||||
|
.expect("get live backup")
|
||||||
|
.expect("backup exists");
|
||||||
|
let expected = serde_json::to_string(&provider_b.settings_config).expect("serialize");
|
||||||
|
assert_eq!(backup.original_config, expected);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn hot_switch_provider_serializes_same_app_switches() {
|
async fn hot_switch_provider_serializes_same_app_switches() {
|
||||||
|
|||||||
Reference in New Issue
Block a user