mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
feat(cursor): proactively renews Cursor sessions in the sweep
Adds src/lib/tokenHealthCheckCursor.ts, sweep-side glue that calls the renewal orchestrator and persists the result, wired into tokenHealthCheck.ts's checkConnection() via a new Cursor-specific branch placed ahead of the generic no-refresh-token fallthrough. Carves out a non-terminal exception for a Cursor connection that already landed at testStatus "expired" via the request-time 401 path, excluding permanently-dead account_deactivated connections. Extends buildRefreshFailureUpdate() with an overrides param so Cursor's failure path can use a distinct, non-terminal errorCode instead of the generic refresh_failed/expired taxonomy.
This commit is contained in:
committed by
diegosouzapw
parent
6ff2eceb9e
commit
71f1804e99
@@ -28,6 +28,7 @@ import {
|
||||
import { pickMaskedDisplayValue } from "@/shared/utils/maskEmail";
|
||||
import { isAutomatedTestProcess } from "@/shared/utils/testProcess";
|
||||
import { refreshGithubCopilotSubTokenIfNeeded } from "@/lib/tokenHealthCheckCopilot";
|
||||
import { checkCursorConnectionIfNeeded } from "@/lib/tokenHealthCheckCursor";
|
||||
|
||||
const LOG_PREFIX = "[HealthCheck]";
|
||||
const TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
|
||||
@@ -119,7 +120,16 @@ export function isInRefreshBackoff(conn: any, nowMs: number): boolean {
|
||||
return Number.isFinite(untilMs) && untilMs > nowMs;
|
||||
}
|
||||
|
||||
export function buildRefreshFailureUpdate(conn: any, now: string) {
|
||||
export function buildRefreshFailureUpdate(
|
||||
conn: any,
|
||||
now: string,
|
||||
overrides?: {
|
||||
errorCode?: string;
|
||||
lastError?: string;
|
||||
lastErrorType?: string;
|
||||
testStatus?: string;
|
||||
}
|
||||
) {
|
||||
const wasExpired = conn.testStatus === "expired";
|
||||
const retryCount = (conn.expiredRetryCount ?? 0) + (wasExpired ? 1 : 0);
|
||||
|
||||
@@ -145,6 +155,7 @@ export function buildRefreshFailureUpdate(conn: any, now: string) {
|
||||
refreshCircuit: { streak, until: getRefreshBackoffUntil(streak, now), lastFailAt: now },
|
||||
},
|
||||
...(wasExpired ? { expiredRetryCount: retryCount, expiredRetryAt: now } : {}),
|
||||
...(overrides || {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -417,11 +428,24 @@ export async function checkConnection(conn) {
|
||||
conn.testStatus === "expired" &&
|
||||
conn.errorCode === "no_refresh_token" &&
|
||||
isGitHubAccessTokenOnlyConnection(conn);
|
||||
// Cursor has no refresh_token by design — an existing REQUEST-TIME path
|
||||
// (resolveTerminalConnectionStatus() in src/sse/services/auth.ts) can land
|
||||
// a Cursor connection at testStatus "expired" on a live 401 before the
|
||||
// Cursor renewal branch below ever runs. Un-terminal it so the sweep can
|
||||
// still attempt a renewal, UNLESS the account is genuinely dead
|
||||
// (lastErrorType "account_deactivated" is documented as permanently dead
|
||||
// and must not be retried — doing so would repeatedly nudge cursor-agent
|
||||
// and re-scrape against a dead account).
|
||||
const isRecoverableCursorExpired =
|
||||
conn.testStatus === "expired" &&
|
||||
String(conn.provider || "").toLowerCase() === "cursor" &&
|
||||
conn.lastErrorType !== "account_deactivated";
|
||||
const terminalStatuses = new Set(["credits_exhausted", "banned", "expired"]);
|
||||
if (
|
||||
typeof conn.testStatus === "string" &&
|
||||
terminalStatuses.has(conn.testStatus.toLowerCase()) &&
|
||||
!isRecoverableGithubCopilotNoRefresh
|
||||
!isRecoverableGithubCopilotNoRefresh &&
|
||||
!isRecoverableCursorExpired
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -453,6 +477,30 @@ export async function checkConnection(conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cursor's refreshToken is always null (no refresh_token by design), so
|
||||
// falling into the generic !conn.refreshToken block below was always a
|
||||
// silent no-op for Cursor. Explicit provider dispatch here is clearer than
|
||||
// relying on that fallthrough.
|
||||
if (String(conn.provider || "").toLowerCase() === "cursor") {
|
||||
const tokenExpiresAt = getEffectiveTokenExpiryMs(conn);
|
||||
const isAboutToExpire = tokenExpiresAt > 0 && tokenExpiresAt - Date.now() < TOKEN_EXPIRY_BUFFER;
|
||||
if (tokenExpiresAt > 0 && !isAboutToExpire) return;
|
||||
if (isInRefreshBackoff(conn, Date.now())) return;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
await checkCursorConnectionIfNeeded({
|
||||
conn,
|
||||
now,
|
||||
buildRefreshFailureUpdate,
|
||||
log,
|
||||
logWarn,
|
||||
logError,
|
||||
getConnectionLogLabel,
|
||||
logPrefix: LOG_PREFIX,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!conn.refreshToken || typeof conn.refreshToken !== "string") {
|
||||
if (isGitHubAccessTokenOnlyConnection(conn)) {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
63
src/lib/tokenHealthCheckCursor.ts
Normal file
63
src/lib/tokenHealthCheckCursor.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Cursor-specific sweep-glue for the proactive token health check. Cursor has
|
||||
* no refresh_token by design — its ~24h import-token is renewed via a
|
||||
* cursor-agent nudge + IDE/agent credential re-scrape (see
|
||||
* src/lib/cursor/renewal.ts), not a standard OAuth refresh_token exchange.
|
||||
*
|
||||
* Sibling to tokenHealthCheckCopilot.ts (same injection-to-avoid-circular-
|
||||
* import technique — tokenHealthCheck.ts-private helpers passed as params
|
||||
* rather than imported, and updateProviderConnection imported directly from
|
||||
* @/lib/localDb) but greenfield: type-checked normally, no @ts-nocheck.
|
||||
*/
|
||||
|
||||
import { updateProviderConnection } from "@/lib/localDb";
|
||||
import {
|
||||
renewCursorConnection,
|
||||
buildCursorRenewedUpdate,
|
||||
runCursorRenewalExclusive,
|
||||
} from "@/lib/cursor/renewal";
|
||||
import type { buildRefreshFailureUpdate } from "@/lib/tokenHealthCheck";
|
||||
|
||||
export async function checkCursorConnectionIfNeeded(params: {
|
||||
conn: any;
|
||||
now: string;
|
||||
buildRefreshFailureUpdate: typeof buildRefreshFailureUpdate;
|
||||
log: (message: string, ...args: any[]) => void;
|
||||
logWarn: (message: string, ...args: any[]) => void;
|
||||
logError: (message: string, ...args: any[]) => void;
|
||||
getConnectionLogLabel: (conn: { name?: string; email?: string; id?: string }) => string;
|
||||
logPrefix: string;
|
||||
}): Promise<void> {
|
||||
const { conn, now, buildRefreshFailureUpdate, log, logWarn, getConnectionLogLabel, logPrefix } =
|
||||
params;
|
||||
|
||||
await runCursorRenewalExclusive(conn.id, async () => {
|
||||
const result = await renewCursorConnection({
|
||||
accessToken: conn.accessToken,
|
||||
machineId: conn.providerSpecificData?.machineId ?? null,
|
||||
});
|
||||
|
||||
if (result.status === "renewed") {
|
||||
await updateProviderConnection(conn.id, buildCursorRenewedUpdate(conn, result, now));
|
||||
log(
|
||||
`${logPrefix} ✓ Cursor session renewed for ${getConnectionLogLabel(conn)} (source: ${result.source})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
result.status === "error"
|
||||
? `Cursor session renewal failed: ${result.error}`
|
||||
: "Cursor session unchanged — no newer token found on this host.";
|
||||
await updateProviderConnection(
|
||||
conn.id,
|
||||
buildRefreshFailureUpdate(conn, now, {
|
||||
errorCode: "cursor_session_stale",
|
||||
lastErrorType: "cursor_session_stale",
|
||||
lastError: message,
|
||||
testStatus: "active",
|
||||
})
|
||||
);
|
||||
logWarn(`${logPrefix} ✗ Cursor session stale for ${getConnectionLogLabel(conn)}: ${message}`);
|
||||
});
|
||||
}
|
||||
@@ -79,11 +79,48 @@ test("isInRefreshBackoff false when no circuit recorded", () => {
|
||||
});
|
||||
|
||||
test("expired connections still track expiredRetryCount AND the circuit", () => {
|
||||
const update = buildRefreshFailureUpdate(
|
||||
{ testStatus: "expired", expiredRetryCount: 1 },
|
||||
NOW
|
||||
);
|
||||
const update = buildRefreshFailureUpdate({ testStatus: "expired", expiredRetryCount: 1 }, NOW);
|
||||
assert.equal(update.testStatus, "expired");
|
||||
assert.equal(update.expiredRetryCount, 2);
|
||||
assert.equal(update.providerSpecificData.refreshCircuit.streak, 1);
|
||||
});
|
||||
|
||||
// Cursor renewal plan, Task 3 Step 1: buildRefreshFailureUpdate() gained an
|
||||
// optional 3rd `overrides` param so Cursor's failure path (which has no
|
||||
// refresh_token by design) can use a distinct errorCode ("cursor_session_stale"
|
||||
// instead of "refresh_failed") and force testStatus:"active" even when the
|
||||
// connection's prior testStatus was already "expired" — without touching any
|
||||
// other provider's error taxonomy or default behavior.
|
||||
test("buildRefreshFailureUpdate applies overrides on top of the defaults, leaving every existing caller (which passes no 3rd arg) byte-identical", () => {
|
||||
const withOverrides = buildRefreshFailureUpdate(
|
||||
{ testStatus: "expired", expiredRetryCount: 0 },
|
||||
NOW,
|
||||
{
|
||||
errorCode: "cursor_session_stale",
|
||||
lastErrorType: "cursor_session_stale",
|
||||
lastError: "Cursor session unchanged — no newer token found on this host.",
|
||||
testStatus: "active",
|
||||
}
|
||||
);
|
||||
assert.equal(withOverrides.errorCode, "cursor_session_stale");
|
||||
assert.equal(withOverrides.lastErrorType, "cursor_session_stale");
|
||||
assert.equal(
|
||||
withOverrides.lastError,
|
||||
"Cursor session unchanged — no newer token found on this host."
|
||||
);
|
||||
assert.equal(
|
||||
withOverrides.testStatus,
|
||||
"active",
|
||||
"the override must force non-terminal status even though wasExpired (prior testStatus) was true"
|
||||
);
|
||||
// wasExpired-derived bookkeeping (retry count, circuit streak) is untouched
|
||||
// by the testStatus override — only the persisted field itself is replaced.
|
||||
assert.equal(withOverrides.expiredRetryCount, 1);
|
||||
assert.equal(withOverrides.providerSpecificData.refreshCircuit.streak, 1);
|
||||
|
||||
const withoutOverrides = buildRefreshFailureUpdate({ testStatus: "active" }, NOW);
|
||||
assert.equal(withoutOverrides.errorCode, "refresh_failed");
|
||||
assert.equal(withoutOverrides.lastErrorType, "token_refresh_failed");
|
||||
assert.equal(withoutOverrides.lastError, "Health check: token refresh failed");
|
||||
assert.equal(withoutOverrides.testStatus, "active");
|
||||
});
|
||||
|
||||
537
tests/unit/token-health-check-cursor.test.ts
Normal file
537
tests/unit/token-health-check-cursor.test.ts
Normal file
@@ -0,0 +1,537 @@
|
||||
/**
|
||||
* Task 3 — wiring the Cursor renewal orchestrator (src/lib/cursor/renewal.ts,
|
||||
* Task 2) into the proactive token health-check sweep (src/lib/tokenHealthCheck.ts
|
||||
* + src/lib/tokenHealthCheckCursor.ts).
|
||||
*
|
||||
* Real DB, real checkConnection()/checkCursorConnectionIfNeeded() — matching
|
||||
* this file family's existing convention (see
|
||||
* tests/unit/token-health-check.test.ts, tests/unit/token-health-no-refresh-token-expired-5326.test.ts):
|
||||
* a real SQLite DB under a temp DATA_DIR, real createProviderConnection()/
|
||||
* updateProviderConnection()/getProviderConnectionById() round-trips, no
|
||||
* mocking of the DB layer.
|
||||
*
|
||||
* checkCursorConnectionIfNeeded() calls the REAL renewCursorConnection()
|
||||
* (Task 2) with no deps override, so — exactly as in
|
||||
* tests/unit/cursor-renewal.test.ts — its dependencies are driven via real
|
||||
* HOME-relative fixture files and a real fake `cursor-agent` binary, never a
|
||||
* mock. This ALSO means the ambient real cursor-agent install on some dev
|
||||
* hosts (see tests/unit/cursor-agent-models.test.ts) must always be shadowed
|
||||
* by a fake binary here too, so no test in this file ever risks invoking a
|
||||
* real cursor-agent process.
|
||||
*/
|
||||
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-cursor-"));
|
||||
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");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
try {
|
||||
if (fs.existsSync(TEST_DATA_DIR)) {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
}
|
||||
break;
|
||||
} catch (error: unknown) {
|
||||
const code =
|
||||
error && typeof error === "object" && "code" in error
|
||||
? (error as { code?: unknown }).code
|
||||
: null;
|
||||
if ((code === "EBUSY" || code === "EPERM") && attempt < 9) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function getId(connection: { id?: unknown }): string {
|
||||
assert.equal(typeof connection.id, "string");
|
||||
return connection.id as string;
|
||||
}
|
||||
|
||||
async function freshConn(id: string) {
|
||||
const conn = await providersDb.getProviderConnectionById(id);
|
||||
assert.ok(conn, `expected connection ${id} to exist`);
|
||||
return conn as Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ---- Real fake cursor-agent binary + IDE/agent fixture helpers, mirroring
|
||||
// tests/unit/cursor-renewal.test.ts and tests/unit/cursor-token-extractor.test.ts ----
|
||||
|
||||
const FAKE_CURSOR_AGENT_SCRIPT = `#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const args = process.argv.slice(2);
|
||||
if (process.env.FAKE_CURSOR_AGENT_LOG) {
|
||||
fs.appendFileSync(process.env.FAKE_CURSOR_AGENT_LOG, JSON.stringify(args) + "\\n");
|
||||
}
|
||||
if (args[0] === "status") {
|
||||
const mode = process.env.FAKE_CURSOR_AGENT_STATUS_MODE || "unauthenticated";
|
||||
if (mode === "authenticated") {
|
||||
process.stdout.write(JSON.stringify({ status: "authenticated", isAuthenticated: true }));
|
||||
} else {
|
||||
process.stdout.write(JSON.stringify({ status: "unauthenticated", isAuthenticated: false }));
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function writeFakeCursorAgentBinary(destPath: string): void {
|
||||
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
||||
fs.writeFileSync(destPath, FAKE_CURSOR_AGENT_SCRIPT, { mode: 0o755 });
|
||||
fs.chmodSync(destPath, 0o755);
|
||||
}
|
||||
|
||||
function readLoggedInvocations(logPath: string): string[][] {
|
||||
if (!fs.existsSync(logPath)) return [];
|
||||
return fs
|
||||
.readFileSync(logPath, "utf-8")
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line));
|
||||
}
|
||||
|
||||
interface CursorEnv {
|
||||
tmpHome: string;
|
||||
logPath: string;
|
||||
writeIdeToken(accessToken: string, machineId?: string): Promise<void>;
|
||||
writeAgentToken(accessToken: string): void;
|
||||
cleanup(): void;
|
||||
}
|
||||
|
||||
async function withCursorEnv<T>(fn: (env: CursorEnv) => Promise<T>): Promise<T> {
|
||||
const originalHome = process.env.HOME;
|
||||
const originalUserProfile = process.env.USERPROFILE;
|
||||
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
|
||||
Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-hc-cursor-env-"));
|
||||
process.env.HOME = tmpHome;
|
||||
process.env.USERPROFILE = tmpHome;
|
||||
|
||||
const logPath = path.join(tmpHome, "log.jsonl");
|
||||
process.env.FAKE_CURSOR_AGENT_LOG = logPath;
|
||||
// Never authenticated by default — the point of these tests is the sweep
|
||||
// wiring/DB-update shapes (Task 2 already covers the nudge itself), and
|
||||
// this also guarantees the real ambient cursor-agent install some hosts
|
||||
// have is never the one actually resolved (this fake one always shadows
|
||||
// it, since it's the first fixed candidate resolveCursorAgentBinary checks).
|
||||
process.env.FAKE_CURSOR_AGENT_STATUS_MODE = "unauthenticated";
|
||||
writeFakeCursorAgentBinary(path.join(tmpHome, ".local", "bin", "cursor-agent"));
|
||||
|
||||
const env: CursorEnv = {
|
||||
tmpHome,
|
||||
logPath,
|
||||
async writeIdeToken(accessToken, machineId) {
|
||||
const { openDatabaseAsync } = await import("../../src/lib/db/adapters/driverFactory.ts");
|
||||
const dbPath = path.join(
|
||||
tmpHome,
|
||||
"Library/Application Support/Cursor/User/globalStorage/state.vscdb"
|
||||
);
|
||||
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
||||
const seed = await openDatabaseAsync(dbPath);
|
||||
seed.exec("CREATE TABLE itemTable (key TEXT PRIMARY KEY, value TEXT)");
|
||||
seed
|
||||
.prepare("INSERT INTO itemTable (key, value) VALUES (?, ?)")
|
||||
.run("cursorAuth/accessToken", accessToken);
|
||||
if (machineId) {
|
||||
seed
|
||||
.prepare("INSERT INTO itemTable (key, value) VALUES (?, ?)")
|
||||
.run("storage.serviceMachineId", machineId);
|
||||
}
|
||||
seed.close();
|
||||
},
|
||||
writeAgentToken(accessToken) {
|
||||
const authDir = path.join(tmpHome, ".config", "cursor");
|
||||
fs.mkdirSync(authDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(authDir, "auth.json"), JSON.stringify({ accessToken }));
|
||||
},
|
||||
cleanup() {
|
||||
if (originalPlatformDescriptor) {
|
||||
Object.defineProperty(process, "platform", originalPlatformDescriptor);
|
||||
}
|
||||
process.env.HOME = originalHome;
|
||||
if (originalUserProfile !== undefined) process.env.USERPROFILE = originalUserProfile;
|
||||
else delete process.env.USERPROFILE;
|
||||
delete process.env.FAKE_CURSOR_AGENT_LOG;
|
||||
delete process.env.FAKE_CURSOR_AGENT_STATUS_MODE;
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
return await fn(env);
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
const NEAR_EXPIRY_ISO = new Date(Date.now() + 60_000).toISOString(); // 1 min out (< 5 min buffer)
|
||||
const PAST_EXPIRY_ISO = new Date(Date.now() - 60 * 60 * 1000).toISOString(); // 1h ago
|
||||
const FAR_FUTURE_ISO = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
async function createCursorConnection(overrides: Record<string, unknown> = {}) {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "cursor",
|
||||
authType: "oauth",
|
||||
email: "cursor-healthcheck@example.com",
|
||||
accessToken: "old-token",
|
||||
refreshToken: null,
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
...overrides,
|
||||
});
|
||||
return getId(connection);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Step 2: checkCursorConnectionIfNeeded DB-update shapes
|
||||
// ============================================================================
|
||||
|
||||
test("checkConnection: Cursor renewed-via-IDE result persists accessToken, ~24h expiry, active status, cleared error fields", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async (env) => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: NEAR_EXPIRY_ISO,
|
||||
expiresAt: NEAR_EXPIRY_ISO,
|
||||
providerSpecificData: { machineId: "old-machine" },
|
||||
});
|
||||
await env.writeIdeToken("new-ide-token", "new-machine");
|
||||
|
||||
const before = Date.now();
|
||||
await tokenHealthCheck.checkConnection(await freshConn(id));
|
||||
const after = Date.now();
|
||||
|
||||
const updated = await freshConn(id);
|
||||
assert.equal(updated.accessToken, "new-ide-token");
|
||||
assert.equal(updated.testStatus, "active");
|
||||
assert.equal(updated.lastError ?? null, null);
|
||||
assert.equal(updated.lastErrorAt ?? null, null);
|
||||
assert.equal(updated.lastErrorType ?? null, null);
|
||||
assert.equal(updated.errorCode ?? null, null);
|
||||
assert.equal(updated.expiredRetryCount ?? null, null);
|
||||
assert.equal(updated.expiredRetryAt ?? null, null);
|
||||
assert.equal(updated.expiresAt, updated.tokenExpiresAt);
|
||||
|
||||
const expiresAtMs = new Date(updated.expiresAt as string).getTime();
|
||||
assert.ok(
|
||||
expiresAtMs >= before + 24 * 60 * 60 * 1000 - 5000 &&
|
||||
expiresAtMs <= after + 24 * 60 * 60 * 1000 + 5000,
|
||||
`expected expiresAt ~24h out, got ${updated.expiresAt}`
|
||||
);
|
||||
|
||||
const psd = updated.providerSpecificData as Record<string, unknown>;
|
||||
assert.equal(psd.machineId, "new-machine");
|
||||
});
|
||||
});
|
||||
|
||||
test('checkConnection: Cursor "unchanged" result marks cursor_session_stale, stays non-terminal (testStatus active, not expired)', async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async (env) => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: NEAR_EXPIRY_ISO,
|
||||
expiresAt: NEAR_EXPIRY_ISO,
|
||||
});
|
||||
// No writeIdeToken/writeAgentToken -> both tokenExtractor functions
|
||||
// gracefully report {found:false} -> renewCursorConnection() returns "unchanged".
|
||||
void env;
|
||||
|
||||
await tokenHealthCheck.checkConnection(await freshConn(id));
|
||||
|
||||
const updated = await freshConn(id);
|
||||
assert.equal(updated.errorCode, "cursor_session_stale");
|
||||
assert.equal(updated.lastErrorType, "cursor_session_stale");
|
||||
assert.match(updated.lastError as string, /Cursor session unchanged/);
|
||||
assert.equal(
|
||||
updated.testStatus,
|
||||
"active",
|
||||
"must NOT be terminal — future sweeps must keep retrying"
|
||||
);
|
||||
assert.ok(updated.lastHealthCheckAt);
|
||||
const psd = updated.providerSpecificData as Record<string, unknown>;
|
||||
assert.equal((psd.refreshCircuit as { streak?: number })?.streak, 1);
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'checkConnection: Cursor "error" result -> DB update shape',
|
||||
{
|
||||
skip:
|
||||
"Same root-cause testability gap as tests/unit/cursor-renewal.test.ts's original " +
|
||||
"case (d), one level up: checkCursorConnectionIfNeeded() (src/lib/tokenHealthCheckCursor.ts) " +
|
||||
"calls the real renewCursorConnection() with NO deps override, so there is no way to force " +
|
||||
'it to return {status:"error"} from here (tryIdeAuth()/tryAgentAuth() never throw for real — ' +
|
||||
"verified in Task 1/2's tests — and this harness has no mock.module() support). " +
|
||||
"renewCursorConnection()'s OWN error-mapping (sanitizeErrorMessage wiring) is already " +
|
||||
"covered directly in tests/unit/cursor-renewal.test.ts's case (d), using its deps seam. " +
|
||||
"The remaining untested surface is narrowly this file's message-building line " +
|
||||
"(`Cursor session renewal failed: ${result.error}`) plus the cursor_session_stale/testStatus:" +
|
||||
'"active" override — both of which ARE exercised by the "unchanged" test above via the ' +
|
||||
"identical buildRefreshFailureUpdate call site (only the interpolated message text differs). " +
|
||||
"Flagged to the team lead/reviewer: forwarding an optional deps param from " +
|
||||
"checkCursorConnectionIfNeeded() through to its internal renewCursorConnection() call " +
|
||||
"(mirroring the seam already added to renewCursorConnection() itself for Task 2) would close " +
|
||||
"this specific gap with a small, additive change.",
|
||||
},
|
||||
async () => {}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Step 1: buildRefreshFailureUpdate's overrides param — DB-shape-adjacent proof
|
||||
// (the pure-function unit test lives in tests/unit/token-health-check-circuit-breaker.test.ts;
|
||||
// this asserts the SAME override plumbing end-to-end through the real DB write above)
|
||||
// ============================================================================
|
||||
|
||||
test("checkConnection: the cursor_session_stale override forces testStatus:active even for a connection whose PRIOR testStatus was expired", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async () => {
|
||||
// A Cursor connection that landed at "expired" via the pre-existing
|
||||
// request-time path (src/sse/services/auth.ts::resolveTerminalConnectionStatus)
|
||||
// — Task 3 Step 3's carve-out lets this reach the branch; Step 1/2's override
|
||||
// must then force it back to non-terminal, not leave/re-derive "expired".
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: PAST_EXPIRY_ISO,
|
||||
expiresAt: PAST_EXPIRY_ISO,
|
||||
testStatus: "expired",
|
||||
});
|
||||
|
||||
await tokenHealthCheck.checkConnection(await freshConn(id));
|
||||
|
||||
const updated = await freshConn(id);
|
||||
assert.equal(
|
||||
updated.testStatus,
|
||||
"active",
|
||||
'buildRefreshFailureUpdate\'s default wasExpired-derived testStatus:"expired" must be overridden'
|
||||
);
|
||||
assert.equal(updated.errorCode, "cursor_session_stale");
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Step 3: terminal-status carve-out (isRecoverableCursorExpired)
|
||||
// ============================================================================
|
||||
|
||||
test("checkConnection: Cursor + expired + no lastErrorType is NOT permanently skipped (reaches the Cursor branch)", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async () => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: PAST_EXPIRY_ISO,
|
||||
expiresAt: PAST_EXPIRY_ISO,
|
||||
testStatus: "expired",
|
||||
// no lastErrorType at all
|
||||
});
|
||||
|
||||
await tokenHealthCheck.checkConnection(await freshConn(id));
|
||||
|
||||
const updated = await freshConn(id);
|
||||
assert.ok(
|
||||
updated.lastHealthCheckAt,
|
||||
"must have reached the Cursor branch (which always writes lastHealthCheckAt)"
|
||||
);
|
||||
assert.equal(updated.testStatus, "active");
|
||||
});
|
||||
});
|
||||
|
||||
test("checkConnection: Cursor + expired + lastErrorType:account_deactivated STAYS permanently skipped", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async () => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: PAST_EXPIRY_ISO,
|
||||
expiresAt: PAST_EXPIRY_ISO,
|
||||
testStatus: "expired",
|
||||
lastErrorType: "account_deactivated",
|
||||
lastHealthCheckAt: null,
|
||||
});
|
||||
const before = await freshConn(id);
|
||||
|
||||
await tokenHealthCheck.checkConnection(before);
|
||||
|
||||
const after = await freshConn(id);
|
||||
assert.deepEqual(after, before, "a permanently-dead account must not be touched at all");
|
||||
});
|
||||
});
|
||||
|
||||
test("checkConnection: a banned Cursor connection stays skipped regardless of lastErrorType", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async () => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: PAST_EXPIRY_ISO,
|
||||
expiresAt: PAST_EXPIRY_ISO,
|
||||
testStatus: "banned",
|
||||
lastErrorType: "some_other_reason",
|
||||
});
|
||||
const before = await freshConn(id);
|
||||
|
||||
await tokenHealthCheck.checkConnection(before);
|
||||
|
||||
const after = await freshConn(id);
|
||||
assert.deepEqual(after, before);
|
||||
});
|
||||
});
|
||||
|
||||
test("checkConnection: a credits_exhausted Cursor connection stays skipped regardless of lastErrorType", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async () => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: PAST_EXPIRY_ISO,
|
||||
expiresAt: PAST_EXPIRY_ISO,
|
||||
testStatus: "credits_exhausted",
|
||||
});
|
||||
const before = await freshConn(id);
|
||||
|
||||
await tokenHealthCheck.checkConnection(before);
|
||||
|
||||
const after = await freshConn(id);
|
||||
assert.deepEqual(after, before);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Step 4: due/backoff dispatch
|
||||
// ============================================================================
|
||||
|
||||
test("checkConnection: Cursor connection NOT near expiry is left completely untouched (no renewal attempt)", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async (env) => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: FAR_FUTURE_ISO,
|
||||
expiresAt: FAR_FUTURE_ISO,
|
||||
});
|
||||
const before = await freshConn(id);
|
||||
|
||||
await tokenHealthCheck.checkConnection(before);
|
||||
|
||||
const after = await freshConn(id);
|
||||
assert.deepEqual(after, before);
|
||||
assert.equal(
|
||||
readLoggedInvocations(env.logPath).length,
|
||||
0,
|
||||
"must never spawn cursor-agent when not due"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("checkConnection: Cursor connection near expiry and NOT in backoff triggers a renewal attempt", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async (env) => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: NEAR_EXPIRY_ISO,
|
||||
expiresAt: NEAR_EXPIRY_ISO,
|
||||
});
|
||||
|
||||
await tokenHealthCheck.checkConnection(await freshConn(id));
|
||||
|
||||
assert.ok(
|
||||
readLoggedInvocations(env.logPath).length >= 1,
|
||||
"expected a cursor-agent availability check"
|
||||
);
|
||||
const updated = await freshConn(id);
|
||||
assert.ok(updated.lastHealthCheckAt);
|
||||
});
|
||||
});
|
||||
|
||||
test("checkConnection: Cursor connection near expiry but currently in backoff is skipped", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async (env) => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
tokenExpiresAt: NEAR_EXPIRY_ISO,
|
||||
expiresAt: NEAR_EXPIRY_ISO,
|
||||
providerSpecificData: {
|
||||
refreshCircuit: { streak: 1, until: new Date(Date.now() + 10 * 60 * 1000).toISOString() },
|
||||
},
|
||||
});
|
||||
const before = await freshConn(id);
|
||||
|
||||
await tokenHealthCheck.checkConnection(before);
|
||||
|
||||
const after = await freshConn(id);
|
||||
assert.deepEqual(after, before);
|
||||
assert.equal(readLoggedInvocations(env.logPath).length, 0, "must not spawn while in backoff");
|
||||
});
|
||||
});
|
||||
|
||||
test("checkConnection: a Cursor connection with no known expiry at all is treated as due", async () => {
|
||||
await resetStorage();
|
||||
await withCursorEnv(async (env) => {
|
||||
const id = await createCursorConnection({
|
||||
accessToken: "old-token",
|
||||
// no tokenExpiresAt/expiresAt at all
|
||||
});
|
||||
|
||||
await tokenHealthCheck.checkConnection(await freshConn(id));
|
||||
|
||||
assert.ok(
|
||||
readLoggedInvocations(env.logPath).length >= 1,
|
||||
"an unknown expiry must be treated as due, not permanently skipped"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Step 5: full sweep-level regression — non-Cursor behavior must be unaffected
|
||||
// ============================================================================
|
||||
|
||||
test("checkConnection: a refresh-capable non-Cursor provider missing its refresh token is still marked expired/no_refresh_token (#5326 unaffected)", async () => {
|
||||
await resetStorage();
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "antigravity",
|
||||
authType: "oauth",
|
||||
name: "Antigravity No-Refresh Account (Cursor-plan regression)",
|
||||
email: "antigravity-cursor-regression@example.com",
|
||||
accessToken: "access-token-only",
|
||||
refreshToken: null,
|
||||
testStatus: "active",
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
await tokenHealthCheck.checkConnection(connection);
|
||||
|
||||
const updated = await providersDb.getProviderConnectionById(getId(connection));
|
||||
assert.equal(updated?.testStatus, "expired");
|
||||
assert.equal(updated?.errorCode, "no_refresh_token");
|
||||
});
|
||||
|
||||
test("checkConnection: a banned non-Cursor connection is still skipped (terminal-status guard unaffected by the Cursor carve-out)", async () => {
|
||||
await resetStorage();
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "oauth",
|
||||
email: "openai-banned-regression@example.com",
|
||||
accessToken: "access-token",
|
||||
refreshToken: "refresh-token",
|
||||
testStatus: "banned",
|
||||
isActive: true,
|
||||
});
|
||||
const before = await providersDb.getProviderConnectionById(getId(connection));
|
||||
|
||||
await tokenHealthCheck.checkConnection(before);
|
||||
|
||||
const after = await providersDb.getProviderConnectionById(getId(connection));
|
||||
assert.deepEqual(after, before);
|
||||
});
|
||||
@@ -34,7 +34,8 @@
|
||||
"open-sse/mcp-server/scopeEnforcement.ts",
|
||||
"open-sse/translator/registry.ts",
|
||||
"open-sse/handlers/responseSanitizer.ts",
|
||||
"open-sse/handlers/responseTranslator.ts"
|
||||
"open-sse/handlers/responseTranslator.ts",
|
||||
"src/lib/tokenHealthCheckCursor.ts"
|
||||
],
|
||||
"exclude": ["node_modules", ".next", "app.__qa_backup", "vscode-extension"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user