diff --git a/src/lib/tokenHealthCheck.ts b/src/lib/tokenHealthCheck.ts index e7ad393a58..6a0091ca7b 100644 --- a/src/lib/tokenHealthCheck.ts +++ b/src/lib/tokenHealthCheck.ts @@ -405,8 +405,23 @@ export async function checkConnection(conn) { // CPU and network on every sweep cycle. Mirrors isTerminalConnectionStatus // in src/sse/services/auth.ts and TERMINAL_CONNECTION_STATUSES in // src/lib/quota/connectionRecovery.ts. + // + // #5326 exception: a GitHub Copilot access-token-only connection parked in + // "expired" with errorCode "no_refresh_token" is NOT actually terminal — it's + // the exact target of the self-heal below (canClearGitHubNoRefreshTokenState), + // which clears that stale status back to "active" once the Copilot sub-token + // proves usable. Treating it as terminal here made that self-heal unreachable, + // leaving healthy Copilot connections stuck at "expired" forever. + const isRecoverableGithubCopilotNoRefresh = + conn.testStatus === "expired" && + conn.errorCode === "no_refresh_token" && + isGitHubAccessTokenOnlyConnection(conn); const terminalStatuses = new Set(["credits_exhausted", "banned", "expired"]); - if (typeof conn.testStatus === "string" && terminalStatuses.has(conn.testStatus.toLowerCase())) { + if ( + typeof conn.testStatus === "string" && + terminalStatuses.has(conn.testStatus.toLowerCase()) && + !isRecoverableGithubCopilotNoRefresh + ) { return; } diff --git a/tests/unit/token-health-no-refresh-token-expired-5326.test.ts b/tests/unit/token-health-no-refresh-token-expired-5326.test.ts index 33a9dd8d61..a79934bc05 100644 --- a/tests/unit/token-health-no-refresh-token-expired-5326.test.ts +++ b/tests/unit/token-health-no-refresh-token-expired-5326.test.ts @@ -174,3 +174,44 @@ test("checkConnection clears stale no_refresh_token state for usable GitHub Copi assert.equal(updated?.lastError ?? null, null); assert.ok(updated?.lastHealthCheckAt); }); + +// Boundary regression for #8182 vs #5326: the terminal-skip guard added by #8182 +// must keep skipping a GitHub Copilot connection that is "expired" for a DIFFERENT +// reason than the recoverable no_refresh_token self-heal above (e.g. a manually +// banned/invalidated account). Only the EXACT no_refresh_token shape is exempted +// from the terminal skip — this proves the #5326 fix did not reopen #8182's +// wasted-probe fix for every "expired" GitHub connection. +test("checkConnection still skips a GitHub Copilot connection expired for a non-no_refresh_token reason (#8182 boundary)", async () => { + await resetStorage(); + + const connection = await providersDb.createProviderConnection({ + provider: "github", + authType: "oauth", + name: "GitHub Genuinely Expired Account", + accessToken: "github-access-token", + refreshToken: null, + providerSpecificData: { + copilotToken: "copilot-token", + copilotTokenExpiresAt: Math.floor((Date.now() + 60 * 60 * 1000) / 1000), + }, + testStatus: "expired", + errorCode: "invalid_grant", + lastError: "Manually invalidated by operator.", + isActive: true, + }); + + await tokenHealthCheck.checkConnection(connection); + + const updated = await providersDb.getProviderConnectionById(getCreatedConnectionId(connection)); + assert.equal( + updated?.testStatus, + "expired", + "terminal-skip must still apply outside the exact no_refresh_token shape" + ); + assert.equal(updated?.errorCode, "invalid_grant"); + assert.equal( + updated?.lastHealthCheckAt ?? null, + null, + "checkConnection must return early without touching the row at all" + ); +});