[codex] preserve optional auth refresh and WIF denies [ci changed_files]

This commit is contained in:
Cooper Gamble
2026-06-17 17:00:15 +00:00
parent 4914889cdd
commit db71258c57
9 changed files with 267 additions and 17 deletions
+24
View File
@@ -801,6 +801,30 @@ async fn auth_result_preserves_external_auth_resolution_errors() {
assert!(manager.auth().await.is_err());
}
#[tokio::test]
async fn optional_auth_does_not_resolve_required_external_auth() {
let cached = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let manager = AuthManager::from_auth_for_testing(cached.clone());
manager.set_external_auth(Arc::new(FailingExternalChatgptAuth(
"required external auth must not run",
)));
let resolved = manager
.auth_for_optional_use()
.await
.expect("cached ChatGPT auth should remain available");
assert_eq!(resolved.get_token().unwrap(), cached.get_token().unwrap());
assert_eq!(
manager
.auth()
.await
.expect_err("normal auth should still require external resolution")
.to_string(),
"required external auth must not run"
);
}
#[tokio::test]
async fn replace_non_required_external_auth_preserves_required_provider() {
let manager = AuthManager::shared(
+24
View File
@@ -1731,6 +1731,30 @@ impl AuthManager {
Ok(self.auth_cached())
}
/// Resolves auth for optional OpenAI-backed features while another model provider is active.
///
/// A required external auth source belongs to the primary model provider and must not be
/// invoked merely to initialize an optional feature. Managed ChatGPT auth and optional
/// external auth still use the normal resolution path, including proactive token refresh.
pub async fn auth_for_optional_use(&self) -> Option<CodexAuth> {
if self
.external_auth()
.is_some_and(|external_auth| external_auth.requires_successful_resolution())
{
return self.auth_cached();
}
match self.auth().await {
Ok(auth) => auth,
Err(error) => {
// The external auth registration can race with the check above. Optional features
// should keep using the last managed credential instead of failing startup.
tracing::error!("Failed to resolve optional feature auth: {error}");
self.auth_cached()
}
}
}
/// Backwards-compatible explicit name for callers that already handle auth resolution errors.
pub async fn auth_result(&self) -> std::io::Result<Option<CodexAuth>> {
self.auth().await
+22 -6
View File
@@ -180,6 +180,16 @@ async fn refresh_token_refreshes_when_auth_is_unchanged() -> Result<()> {
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn auth_refreshes_when_access_token_is_near_expiry() -> Result<()> {
assert_auth_refreshes_when_access_token_is_near_expiry(/*optional*/ false).await
}
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn optional_auth_refreshes_when_access_token_is_near_expiry() -> Result<()> {
assert_auth_refreshes_when_access_token_is_near_expiry(/*optional*/ true).await
}
async fn assert_auth_refreshes_when_access_token_is_near_expiry(optional: bool) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
@@ -208,12 +218,18 @@ async fn auth_refreshes_when_access_token_is_near_expiry() -> Result<()> {
};
ctx.write_auth(&initial_auth).await?;
let cached_auth = ctx
.auth_manager
.auth()
.await
.context("auth should resolve")?
.context("auth should be cached")?;
let cached_auth = if optional {
ctx.auth_manager
.auth_for_optional_use()
.await
.context("optional auth should be cached")?
} else {
ctx.auth_manager
.auth()
.await
.context("auth should resolve")?
.context("auth should be cached")?
};
let refreshed_tokens = TokenData {
access_token: "new-access-token".to_string(),