mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-31 11:01:36 +08:00
feat(codex): preserve OAuth login state during third-party provider switching
Codex provider switches now only write config.toml for third-party providers, injecting the API key as experimental_bearer_token. The user's auth.json (ChatGPT OAuth tokens) is preserved. Official providers with login material still write auth.json normally. Backfill restores bearer tokens into stored provider auth.OPENAI_API_KEY to maintain canonical shape.
This commit is contained in:
@@ -70,14 +70,14 @@ fn sync_claude_provider_writes_live_settings() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_codex_provider_writes_auth_and_config() {
|
||||
fn sync_codex_provider_writes_config_without_touching_auth() {
|
||||
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 基础配置的写入
|
||||
// Codex provider 切换只写 config.toml;auth.json 保留用户登录态。
|
||||
|
||||
let provider_config = json!({
|
||||
"auth": {
|
||||
@@ -105,8 +105,8 @@ fn sync_codex_provider_writes_auth_and_config() {
|
||||
let config_path = cc_switch_lib::get_codex_config_path();
|
||||
|
||||
assert!(
|
||||
auth_path.exists(),
|
||||
"auth.json should exist at {}",
|
||||
!auth_path.exists(),
|
||||
"auth.json should not be created by provider switching at {}",
|
||||
auth_path.display()
|
||||
);
|
||||
assert!(
|
||||
@@ -115,20 +115,16 @@ fn sync_codex_provider_writes_auth_and_config() {
|
||||
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"
|
||||
);
|
||||
assert!(
|
||||
toml_text.contains("experimental_bearer_token"),
|
||||
"config.toml should contain provider-scoped bearer token"
|
||||
);
|
||||
|
||||
// 当前供应商应同步最新 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
|
||||
@@ -136,7 +132,81 @@ fn sync_codex_provider_writes_auth_and_config() {
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("config string");
|
||||
assert_eq!(synced_cfg, toml_text);
|
||||
assert!(
|
||||
!synced_cfg.contains("experimental_bearer_token"),
|
||||
"provider storage should not persist generated live bearer token"
|
||||
);
|
||||
assert!(
|
||||
toml_text.contains("experimental_bearer_token"),
|
||||
"live config should include generated bearer token"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_codex_provider_with_config_only_token_backfills_auth() {
|
||||
// P2-2 回归: stored provider 的 token 只藏在 config.toml 的 experimental_bearer_token 时,
|
||||
// sync 路径必须把 token 从 live config 提取并写回 stored auth.OPENAI_API_KEY,
|
||||
// 否则下一轮 sync 会在 cleaned config + 空 auth 之间丢失 token。
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
|
||||
let mut config = MultiAppConfig::default();
|
||||
|
||||
let stored_config = r#"model_provider = "thirdparty"
|
||||
model = "gpt-5.4"
|
||||
|
||||
[model_providers.thirdparty]
|
||||
name = "Thirdparty"
|
||||
base_url = "https://thirdparty.example/v1"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = true
|
||||
experimental_bearer_token = "stored-bearer-key"
|
||||
"#;
|
||||
|
||||
let provider = Provider::with_id(
|
||||
"thirdparty-1".to_string(),
|
||||
"Thirdparty".to_string(),
|
||||
json!({
|
||||
"auth": {},
|
||||
"config": stored_config,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
let manager = config
|
||||
.get_manager_mut(&AppType::Codex)
|
||||
.expect("codex manager");
|
||||
manager
|
||||
.providers
|
||||
.insert("thirdparty-1".to_string(), provider);
|
||||
manager.current = "thirdparty-1".to_string();
|
||||
|
||||
ConfigService::sync_current_providers_to_live(&mut config).expect("sync codex live");
|
||||
|
||||
let manager = config.get_manager(&AppType::Codex).expect("codex manager");
|
||||
let synced = manager
|
||||
.providers
|
||||
.get("thirdparty-1")
|
||||
.expect("provider survives sync");
|
||||
|
||||
assert_eq!(
|
||||
synced
|
||||
.settings_config
|
||||
.pointer("/auth/OPENAI_API_KEY")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("stored-bearer-key"),
|
||||
"config-only bearer token must be backfilled into stored auth.OPENAI_API_KEY"
|
||||
);
|
||||
|
||||
let synced_cfg = synced
|
||||
.settings_config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("config string");
|
||||
assert!(
|
||||
!synced_cfg.contains("experimental_bearer_token"),
|
||||
"live-only bearer token should not be persisted in stored provider config"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -221,8 +291,8 @@ requires_openai_auth = true
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("synced config string");
|
||||
assert!(
|
||||
synced_cfg.contains("[model_providers.custom]"),
|
||||
"ConfigService keeps its existing behavior of syncing provider config from live"
|
||||
synced_cfg.contains("[model_providers.aihubmix]"),
|
||||
"ConfigService should restore the provider-specific id before writing stored config"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user