From 60a9b330e5342a4f04a4174b7bd51b82c5899146 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 30 May 2026 22:09:28 +0800 Subject: [PATCH] Refactor Codex live-write routing and cover default auth overwrite Collapse the two duplicated write_codex_live_atomic branches in write_codex_live_for_provider into a single should_write_auth guard. This is behavior-preserving: `if A {X} else if B {X} else {Y}` becomes `if A || B {X} else {Y}`. Adapt the Codex switch tests to the new opt-in default for preserve_codex_official_auth_on_switch (flipped off in 3f59ab37): add an enable_codex_official_auth_preservation() test helper for the cases that assert the auth-preserving path, and tag the official login provider with category="official" so it routes through the official branch rather than relying on the global preservation flag. Add a regression test locking the default (preservation off) behavior: switching to a third-party provider rewrites auth.json with the new API key and discards the existing ChatGPT OAuth login. This is the dual of the existing preserve-and-backfill test, which only covered the opt-in path. --- src-tauri/src/codex_config.rs | 10 +-- src-tauri/tests/import_export_sync.rs | 4 +- src-tauri/tests/provider_commands.rs | 4 +- src-tauri/tests/provider_service.rs | 114 +++++++++++++++++++++++--- src-tauri/tests/support.rs | 9 ++ 5 files changed, 123 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index 20ecc1606..9355adc02 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -796,11 +796,11 @@ pub fn write_codex_live_for_provider( auth: &Value, config_text: Option<&str>, ) -> Result<(), AppError> { - if category == Some("official") && codex_auth_has_login_material(auth) { - write_codex_live_atomic(auth, config_text) - } else if category != Some("official") - && !crate::settings::preserve_codex_official_auth_on_switch() - { + let should_write_auth = (category == Some("official") && codex_auth_has_login_material(auth)) + || (category != Some("official") + && !crate::settings::preserve_codex_official_auth_on_switch()); + + if should_write_auth { write_codex_live_atomic(auth, config_text) } else { let live_config = prepare_codex_provider_live_config(auth, config_text.unwrap_or(""))?; diff --git a/src-tauri/tests/import_export_sync.rs b/src-tauri/tests/import_export_sync.rs index 380c58730..e1e81d24e 100644 --- a/src-tauri/tests/import_export_sync.rs +++ b/src-tauri/tests/import_export_sync.rs @@ -10,7 +10,8 @@ use cc_switch_lib::{ #[path = "support.rs"] mod support; use support::{ - create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex, + create_test_state, create_test_state_with_config, enable_codex_official_auth_preservation, + ensure_test_home, reset_test_fs, test_mutex, }; #[test] @@ -73,6 +74,7 @@ fn sync_claude_provider_writes_live_settings() { fn sync_codex_provider_writes_config_without_touching_auth() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); + enable_codex_official_auth_preservation(); let mut config = MultiAppConfig::default(); diff --git a/src-tauri/tests/provider_commands.rs b/src-tauri/tests/provider_commands.rs index 8fa723067..4dc59459f 100644 --- a/src-tauri/tests/provider_commands.rs +++ b/src-tauri/tests/provider_commands.rs @@ -11,7 +11,8 @@ use cc_switch_lib::{ mod support; use std::collections::HashMap; use support::{ - create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex, + create_test_state, create_test_state_with_config, enable_codex_official_auth_preservation, + ensure_test_home, reset_test_fs, test_mutex, }; fn settings_path(home: &Path) -> PathBuf { @@ -238,6 +239,7 @@ fn codex_startup_import_skips_when_only_official_seed_exists() { fn switch_provider_updates_codex_live_and_state() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); + enable_codex_official_auth_preservation(); let _home = ensure_test_home(); let legacy_auth = json!({"OPENAI_API_KEY": "legacy-key"}); diff --git a/src-tauri/tests/provider_service.rs b/src-tauri/tests/provider_service.rs index 8eb1b0345..c8152edaa 100644 --- a/src-tauri/tests/provider_service.rs +++ b/src-tauri/tests/provider_service.rs @@ -8,7 +8,8 @@ use cc_switch_lib::{ #[path = "support.rs"] mod support; use support::{ - create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex, + create_test_state, create_test_state_with_config, enable_codex_official_auth_preservation, + ensure_test_home, reset_test_fs, test_mutex, }; fn sanitize_provider_name(name: &str) -> String { @@ -99,6 +100,7 @@ fn migrate_legacy_common_config_usage_marks_historical_provider_enabled() { fn provider_service_switch_codex_updates_live_and_config() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); + enable_codex_official_auth_preservation(); let _home = ensure_test_home(); let legacy_auth = json!({ "OPENAI_API_KEY": "legacy-key" }); @@ -355,6 +357,7 @@ requires_openai_auth = true fn provider_service_switch_codex_preserves_oauth_and_backfills_api_key_from_live_token() { let _guard = test_mutex().lock().expect("acquire test mutex"); reset_test_fs(); + enable_codex_official_auth_preservation(); let _home = ensure_test_home(); let live_auth = json!({ @@ -520,6 +523,94 @@ requires_openai_auth = true ); } +#[test] +fn provider_service_switch_codex_default_overwrites_official_auth_when_preservation_off() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + // Intentionally do NOT enable preservation: this locks the default opt-out + // behavior where switching to a third-party provider rewrites auth.json, + // discarding the user's ChatGPT OAuth login. It is the dual of + // `provider_service_switch_codex_preserves_oauth_and_backfills_api_key_from_live_token`. + let _home = ensure_test_home(); + + let live_auth = json!({ + "auth_mode": "chatgpt", + "OPENAI_API_KEY": null, + "tokens": { + "access_token": "official-oauth-token", + "account_id": "acct-1" + } + }); + let legacy_config = r#"model_provider = "rightcode" +model = "gpt-5.4" + +[model_providers.rightcode] +name = "RightCode" +base_url = "https://rightcode.example/v1" +wire_api = "responses" +requires_openai_auth = true +"#; + write_codex_live_atomic(&live_auth, Some(legacy_config)) + .expect("seed existing Codex OAuth live config"); + + let mut initial_config = MultiAppConfig::default(); + { + let manager = initial_config + .get_manager_mut(&AppType::Codex) + .expect("codex manager"); + manager.current = "legacy-provider".to_string(); + manager.providers.insert( + "legacy-provider".to_string(), + Provider::with_id( + "legacy-provider".to_string(), + "RightCode".to_string(), + json!({ + "auth": {"OPENAI_API_KEY": "rightcode-key"}, + "config": legacy_config + }), + None, + ), + ); + manager.providers.insert( + "third-party".to_string(), + Provider::with_id( + "third-party".to_string(), + "AiHubMix".to_string(), + json!({ + "auth": {"OPENAI_API_KEY": "third-party-key"}, + "config": r#"model_provider = "aihubmix" +model = "gpt-5.4" + +[model_providers.aihubmix] +name = "AiHubMix" +base_url = "https://aihubmix.example/v1" +wire_api = "responses" +requires_openai_auth = true +"# + }), + None, + ), + ); + } + + let state = create_test_state_with_config(&initial_config).expect("create test state"); + + ProviderService::switch(&state, AppType::Codex, "third-party") + .expect("switch to third-party provider should succeed"); + + let auth_value: serde_json::Value = + read_json_file(&cc_switch_lib::get_codex_auth_path()).expect("read auth.json"); + assert_eq!( + auth_value.get("OPENAI_API_KEY").and_then(|v| v.as_str()), + Some("third-party-key"), + "default (preservation off) should overwrite auth.json with the third-party API key" + ); + assert!( + auth_value.pointer("/tokens/access_token").is_none(), + "default switch must clear the official ChatGPT OAuth token from live auth.json" + ); +} + #[test] fn provider_service_switch_codex_supports_official_login_provider_without_auth_write() { let _guard = test_mutex().lock().expect("acquire test mutex"); @@ -561,18 +652,19 @@ requires_openai_auth = true None, ), ); - manager.providers.insert( + let mut official_provider = Provider::with_id( "official-provider".to_string(), - Provider::with_id( - "official-provider".to_string(), - "OpenAI Official".to_string(), - json!({ - "auth": {}, - "config": "" - }), - None, - ), + "OpenAI Official".to_string(), + json!({ + "auth": {}, + "config": "" + }), + None, ); + official_provider.category = Some("official".to_string()); + manager + .providers + .insert("official-provider".to_string(), official_provider); } let state = create_test_state_with_config(&initial_config).expect("create test state"); diff --git a/src-tauri/tests/support.rs b/src-tauri/tests/support.rs index 86f8f76e0..3fd9602b7 100644 --- a/src-tauri/tests/support.rs +++ b/src-tauri/tests/support.rs @@ -50,6 +50,15 @@ pub fn reset_test_fs() { let _ = update_settings(AppSettings::default()); } +#[allow(dead_code)] +pub fn enable_codex_official_auth_preservation() { + update_settings(AppSettings { + preserve_codex_official_auth_on_switch: true, + ..Default::default() + }) + .expect("enable Codex official auth preservation"); +} + /// 全局互斥锁,避免多测试并发写入相同的 HOME 目录。 pub fn test_mutex() -> &'static Mutex<()> { static MUTEX: OnceLock> = OnceLock::new();