From 37e295fc029070a214e2690d08429791fcac1512 Mon Sep 17 00:00:00 2001 From: Ayush Ojha Date: Fri, 30 Jan 2026 07:39:17 -0800 Subject: [PATCH] fix: don't warn about expired OAuth tokens with valid refresh tokens (#4593) OAuth credentials with a refresh token auto-renew on first API call, so the doctor should not warn about access token expiration when a refresh token is present. This avoids unnecessary "expired" warnings that prompt users to re-auth when no action is needed. Fixes #3032 Co-authored-by: Ayush Ojha --- src/agents/auth-health.test.ts | 34 +++++++++++++++++++++++++++++++--- src/agents/auth-health.ts | 11 ++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/agents/auth-health.test.ts b/src/agents/auth-health.test.ts index a7797e17e43..80b38a6c6f4 100644 --- a/src/agents/auth-health.test.ts +++ b/src/agents/auth-health.test.ts @@ -52,11 +52,39 @@ describe("buildAuthHealthSummary", () => { ); expect(statuses["anthropic:ok"]).toBe("ok"); - expect(statuses["anthropic:expiring"]).toBe("expiring"); - expect(statuses["anthropic:expired"]).toBe("expired"); + // OAuth credentials with refresh tokens are auto-renewable, so they report "ok" + expect(statuses["anthropic:expiring"]).toBe("ok"); + expect(statuses["anthropic:expired"]).toBe("ok"); expect(statuses["anthropic:api"]).toBe("static"); const provider = summary.providers.find((entry) => entry.provider === "anthropic"); - expect(provider?.status).toBe("expired"); + expect(provider?.status).toBe("ok"); + }); + + it("reports expired for OAuth without a refresh token", () => { + vi.spyOn(Date, "now").mockReturnValue(now); + const store = { + version: 1, + profiles: { + "google:no-refresh": { + type: "oauth" as const, + provider: "google-antigravity", + access: "access", + refresh: "", + expires: now - 10_000, + }, + }, + }; + + const summary = buildAuthHealthSummary({ + store, + warnAfterMs: DEFAULT_OAUTH_WARN_MS, + }); + + const statuses = Object.fromEntries( + summary.profiles.map((profile) => [profile.profileId, profile.status]), + ); + + expect(statuses["google:no-refresh"]).toBe("expired"); }); }); diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts index c039d81be03..4f281b7bb51 100644 --- a/src/agents/auth-health.ts +++ b/src/agents/auth-health.ts @@ -123,7 +123,16 @@ function buildProfileHealth(params: { }; } - const { status, remainingMs } = resolveOAuthStatus(credential.expires, now, warnAfterMs); + const hasRefreshToken = typeof credential.refresh === "string" && credential.refresh.length > 0; + const { status: rawStatus, remainingMs } = resolveOAuthStatus( + credential.expires, + now, + warnAfterMs, + ); + // OAuth credentials with a valid refresh token auto-renew on first API call, + // so don't warn about access token expiration. + const status = + hasRefreshToken && (rawStatus === "expired" || rawStatus === "expiring") ? "ok" : rawStatus; return { profileId, provider: credential.provider,