Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 92 additions & 4 deletions src-tauri/src/codex_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1050,18 +1050,39 @@ pub fn write_codex_live_for_provider(
auth: &Value,
config_text: Option<&str>,
) -> Result<(), AppError> {
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 category == Some("official") {
return write_codex_official_live_for_provider(auth, config_text);
}

if should_write_auth {
if !crate::settings::preserve_codex_official_auth_on_switch() {
write_codex_live_atomic(auth, config_text)
} else {
let live_config = prepare_codex_provider_live_config(auth, config_text.unwrap_or(""))?;
write_codex_live_config_atomic(Some(&live_config))
}
}

fn write_codex_official_live_for_provider(
auth: &Value,
config_text: Option<&str>,
) -> Result<(), AppError> {
if codex_auth_has_login_material(auth) {
return write_codex_live_atomic(auth, config_text);
}

let auth_path = get_codex_auth_path();
if auth_path.exists() {
let mut live_auth: Value = read_json_file(&auth_path)?;
if let Some(live_auth_obj) = live_auth.as_object_mut() {
if live_auth_obj.remove("OPENAI_API_KEY").is_some() {
return write_codex_live_atomic(&live_auth, config_text);
}
}
}

write_codex_live_config_atomic(config_text)
}
Comment on lines +1065 to +1088

/// Build the live Codex config for provider switching.
///
/// The stored provider keeps its API key in `auth.OPENAI_API_KEY`. Live Codex
Expand Down Expand Up @@ -1253,6 +1274,34 @@ pub fn remove_codex_toml_base_url_if(toml_str: &str, predicate: impl Fn(&str) ->
mod tests {
use super::*;
use serde_json::json;
use serial_test::serial;
use std::ffi::OsString;

struct TestHomeGuard {
_dir: tempfile::TempDir,
old_test_home: Option<OsString>,
}

impl TestHomeGuard {
fn new() -> Self {
let dir = tempfile::tempdir().expect("create temp home");
let old_test_home = std::env::var_os("CC_SWITCH_TEST_HOME");
std::env::set_var("CC_SWITCH_TEST_HOME", dir.path());
Self {
_dir: dir,
old_test_home,
}
}
}

impl Drop for TestHomeGuard {
fn drop(&mut self) {
match &self.old_test_home {
Some(value) => std::env::set_var("CC_SWITCH_TEST_HOME", value),
None => std::env::remove_var("CC_SWITCH_TEST_HOME"),
}
}
}
Comment on lines +1289 to +1308

#[test]
fn prepare_provider_live_config_rejects_key_without_config() {
Expand Down Expand Up @@ -1333,6 +1382,45 @@ experimental_bearer_token = "stale-table-key"
);
}

#[test]
#[serial]
fn official_provider_empty_auth_clears_stale_live_api_key_without_dropping_oauth() {
let _guard = TestHomeGuard::new();
let auth_path = get_codex_auth_path();
fs::create_dir_all(auth_path.parent().unwrap()).expect("create codex dir");
write_json_file(
&auth_path,
&json!({
"auth_mode": "chatgpt",
"tokens": {
"access_token": "oauth-access",
"refresh_token": "oauth-refresh"
},
"OPENAI_API_KEY": "sk-stale"
}),
)
.expect("seed live auth");

write_codex_live_for_provider(Some("official"), &json!({}), Some("model = \"gpt-5\"\n"))
.expect("save official provider");

let saved_auth: Value = read_json_file(&auth_path).expect("read saved auth");
assert_eq!(
saved_auth
.pointer("/tokens/access_token")
.and_then(Value::as_str),
Some("oauth-access")
);
assert!(
saved_auth.get("OPENAI_API_KEY").is_none(),
"stale API key should be removed while OAuth tokens remain"
);
assert_eq!(
fs::read_to_string(get_codex_config_path()).expect("read saved config"),
"model = \"gpt-5\"\n"
);
}

#[test]
fn prepare_provider_live_config_does_not_create_incomplete_provider_table() {
let input = r#"model_provider = "vendor_x"
Expand Down
Loading