diff --git a/open-sse/services/tokenRefresh.ts b/open-sse/services/tokenRefresh.ts index c978828592..70535d98dc 100755 --- a/open-sse/services/tokenRefresh.ts +++ b/open-sse/services/tokenRefresh.ts @@ -472,7 +472,11 @@ export function supportsTokenRefresh(provider) { "cline", "kimi-coding", "windsurf", - "devin-cli", + // #8407: do NOT list "devin-cli" here. It is import-token / local-CLI owned + // (`devin auth login`); connections never carry a refresh token. Leaving it + // in this set made tokenHealthCheck treat it as refresh-capable and force + // testStatus="expired" / errorCode="no_refresh_token". Keep it out of the + // explicit set (same idea as not listing non-refresh local-CLI providers). "gitlab-duo", "codebuddy-cn", ]); diff --git a/src/lib/tokenHealthCheck.ts b/src/lib/tokenHealthCheck.ts index 6a0091ca7b..658c645177 100644 --- a/src/lib/tokenHealthCheck.ts +++ b/src/lib/tokenHealthCheck.ts @@ -513,6 +513,8 @@ export async function checkConnection(conn) { // cosmetic "Token Expired". Surface reality as a terminal "expired" status instead. // Guard tightly so we do NOT clobber: // - providers that simply don't use refresh tokens (supportsTokenRefresh=false) + // (see #8407: local-CLI import-token providers like devin-cli must not be + // listed in supportsTokenRefresh's explicit set) // - connections already in a terminal/specific state (expired/banned/credits_exhausted) // - transient cooldown state (unavailable) owned by the request path const refreshCapableNeedsReauth = diff --git a/tests/unit/token-health-check-devin-cli-8407.test.ts b/tests/unit/token-health-check-devin-cli-8407.test.ts new file mode 100644 index 0000000000..9812b21615 --- /dev/null +++ b/tests/unit/token-health-check-devin-cli-8407.test.ts @@ -0,0 +1,67 @@ +// #8407: devin-cli must not be treated as refresh-capable, so the health sweep +// never force-expires local CLI connections that legitimately have no refresh token. +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +process.env.NODE_ENV = "test"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-hc-devin-cli-8407-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const tokenHealthCheck = await import("../../src/lib/tokenHealthCheck.ts"); +const { supportsTokenRefresh } = await import("../../open-sse/services/tokenRefresh.ts"); + +async function resetStorage() { + core.resetDbInstance(); + if (fs.existsSync(TEST_DATA_DIR)) { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + } + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +function getCreatedConnectionId(connection: { id?: unknown }): string { + assert.equal(typeof connection.id, "string"); + return connection.id as string; +} + +test.after(async () => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("supportsTokenRefresh excludes devin-cli (#8407)", () => { + // Root fix: drop "devin-cli" from the explicit set so the health sweep's + // supportsTokenRefresh=false guard applies (same idea as not listing a + // non-refresh local-CLI provider). windsurf stays refresh-capable. + assert.equal( + supportsTokenRefresh("devin-cli"), + false, + "devin-cli is local import-token / CLI-owned — not refresh-capable" + ); + assert.equal(supportsTokenRefresh("windsurf"), true); +}); + +test("checkConnection leaves a devin-cli connection with no refresh token untouched (#8407)", async () => { + await resetStorage(); + + const connection = await providersDb.createProviderConnection({ + provider: "devin-cli", + authType: "oauth", + name: "Devin CLI Local Account", + accessToken: "local-cli-access-token", + refreshToken: null, + testStatus: "active", + isActive: true, + }); + + await tokenHealthCheck.checkConnection(connection); + + const updated = await providersDb.getProviderConnectionById(getCreatedConnectionId(connection)); + assert.equal(updated?.testStatus, "active", "devin-cli testStatus must remain active"); + assert.notEqual(updated?.errorCode, "no_refresh_token", "devin-cli must not be marked no_refresh_token"); +});