Files
OmniRoute/tests/unit/gemini-cli-legacy-refresh.test.ts
Diego Rodrigues de Sa e Souza 2c243cf1fc feat(sse): deprecate the gemini-cli upstream provider with a real migration path (#8980)
* feat(sse): deprecate the gemini-cli upstream provider with a real migration path

Stored `gemini-cli` connections were being kept alive for nothing. Measured before
touching anything:

  routable?    absent from PROVIDERS, from REGISTRY, from OAUTH_PROVIDERS, and no
               executor references it → the connection can NEVER serve a request
  refreshing?  yes, and successfully — it redeemed against PROVIDERS.gemini's client
               (681255809395-oo8ft2o…), the same public Gemini CLI / Code Assist OAuth
               client

So the scheduler made periodic upstream calls to Google to keep a credential fresh
that had nowhere to go. That is the waste this removes.

This is a deprecation, not a deletion, and the difference is deliberate. The path was
not dead code: #8232 added it after a user report (the UI advertises automatic OAuth
rotation and these rows never rotated), and #8275 narrowed it to exactly the legacy
refresh. Simply dropping it from `supportsTokenRefresh` would have produced a SILENT
skip — `Skipping … (refresh unsupported)` — leaving the row at "active" forever, doing
nothing. Worse than before.

Instead:

  DEPRECATED_PROVIDERS + isDeprecatedProvider/getDeprecationNotice in tokenRefresh
      one place naming the provider and where to migrate. A test asserts the migration
      target is itself routable, so the notice can never point somewhere useless.

  _getAccessTokenInternal returns the ESTABLISHED unrecoverable envelope
      { error: "unrecoverable_refresh_error", code: "provider_deprecated", migrateTo }
      Reusing `error` means isUnrecoverableRefreshError and the manual-refresh route
      already stop retrying — no new contract for callers to learn. The distinct `code`
      is what makes it legible. A bare `null` would read as transient and retry forever.

  tokenHealthCheck marks the connection terminal with the reason
      Placed after the existing terminal-status guard, which makes it idempotent for
      free: once "expired", later sweeps skip the row, so it writes once instead of
      rewriting the same reason every cycle.

  the manual-refresh route stops lying
      It said "Refresh token expired. Please re-authenticate this account." — false
      here: the token is fine, the provider is gone. Re-authenticating would loop
      against something that no longer exists. It now reports the deprecation and the
      migration target.

`gemini` uses the same OAuth client, so re-adding the account there is a working path,
not advice to start over.

Deliberately NOT touched:

  Category A — the gemini-cli CLIENT identity (#7034): clientIdentityProfiles.ts,
      clientApi.ts, googApiKeyAuth.ts. Same string, opposite direction — requests
      ARRIVING from the Gemini CLI, where OmniRoute is the server. Deleting these is the
      failure this change must never cause, so a test now asserts the profile survives.
      Audited: `git diff --name-only` touches none of those files.

  errorClassifier.ts's isCloudCodeProvider list still names gemini-cli. It is a
      defensive 403→PROJECT_ROUTE_ERROR list shared with cloudcode/cloud-code; the entry
      is unreachable for a non-routable provider, and editing a shared classification
      path for a dead string is risk without upside.

Tests — 42 across the six files that mention the identifier, all green:

    gemini-cli-legacy-refresh.test.ts        5   (3 assertions REWRITTEN, see below)
    gemini-cli-deprecation.test.ts           5   (new)
    client-identity-profiles.test.ts         9   (category A, untouched)
    service-token-refresh.test.ts           14
    errorclassifier-antigravity-403.test.ts  4
    gemini-cli-ansi-sanitization.test.ts     5   (category C, untouched)

The three rewritten assertions in the legacy file are alignment, not weakening, and the
gate is right to ask: each is now STRONGER. "refresh succeeds against Google's token
endpoint" became "zero upstream calls happen at all"; "a 400 surfaces invalid_grant"
became "the envelope is unchanged but the code says provider_deprecated" plus a control
asserting `gemini` still reports invalid_grant, proving the real path was not blunted.
The file's header keeps the whole #8232#8275 → deprecation arc, because each step is
why the next made sense. Count unchanged; no test deleted, so no allowlist entry needed.

* docs(changelog): fragment for #8980

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-07-30 09:36:54 -03:00

129 lines
5.9 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { OAUTH_ENDPOINTS, PROVIDERS as LEGACY_PROVIDERS } from "../../open-sse/config/constants.ts";
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
import {
getAccessToken,
REFRESH_LEAD_MS,
supportsTokenRefresh,
} from "../../open-sse/services/tokenRefresh.ts";
import { OAUTH_PROVIDERS } from "../../src/shared/constants/providers.ts";
// The arc of this file, kept whole because each step is the reason the next made sense:
//
// #8232 Restored OAuth auto-refresh for stored `gemini-cli` connections — a real user
// report: the UI advertises automatic token rotation for OAuth providers, and
// these rows never rotated. It overshot, restoring a routable, UI-visible
// provider along the way.
// #8275 Narrowed that to the legacy refresh path ONLY, keeping the discontinued
// provider out of the public registries and out of routing.
// now Deprecated. What #8275 left was a refresh that WORKED (it redeemed against
// PROVIDERS.gemini's client — the same public Gemini CLI OAuth client) for a
// provider that is NOT routable. So the token stayed fresh and could never
// answer a request: periodic upstream calls maintaining a dead credential.
//
// The refresh assertions below therefore now assert the deprecation instead of the
// refresh. They were rewritten, not removed — the count is unchanged and the behavior is
// pinned harder than before (a silent skip would pass a weaker test; a classified code
// does not). Registry-exclusion coverage from #8275 is untouched, because that guarantee
// still holds and is still worth guarding.
//
// Companion: tests/unit/gemini-cli-deprecation.test.ts covers the notice itself, the
// routability of the migration target, and the untouched CLIENT identity (#7034).
test("Gemini CLI stays out of the chat and OAuth provider registries", () => {
assert.equal(REGISTRY["gemini-cli"], undefined);
assert.equal(LEGACY_PROVIDERS["gemini-cli"], undefined);
assert.equal((OAUTH_PROVIDERS as Record<string, unknown>)["gemini-cli"], undefined);
assert.ok(REGISTRY.gemini);
assert.ok(REGISTRY.antigravity);
});
test("legacy Gemini CLI connections are no longer refreshed at all", () => {
// Was: lead time equal to antigravity's, supportsTokenRefresh === true.
assert.equal(supportsTokenRefresh("gemini-cli"), false);
assert.equal(REFRESH_LEAD_MS["gemini-cli"], undefined);
// The sibling Google-backed providers must NOT be affected by the deprecation.
assert.equal(supportsTokenRefresh("gemini"), true);
assert.equal(REFRESH_LEAD_MS.antigravity, 15 * 60 * 1000);
});
test("Gemini CLI stays out of the provider translation snapshot", () => {
const snapshotPath = new URL("../snapshots/provider/translate-path.json", import.meta.url);
const snapshot = JSON.parse(readFileSync(snapshotPath, "utf8")) as Record<string, unknown>;
assert.equal(snapshot["gemini-cli"], undefined);
});
test("legacy Gemini CLI refresh never reaches Google's token endpoint anymore", async () => {
// Was: asserted a successful POST to OAUTH_ENDPOINTS.google.token carrying
// PROVIDERS.gemini's client_id/secret, returning a new access token. That call is the
// waste the deprecation removes — the token it produced could not route anywhere. Now
// the assertion is stronger: not "it fails", but "no upstream call happens at all".
const originalFetch = globalThis.fetch;
const calls: string[] = [];
globalThis.fetch = (async (url) => {
calls.push(String(url));
return new Response(JSON.stringify({ access_token: "should-never-be-requested" }), {
status: 200,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
try {
const result = await getAccessToken(
"gemini-cli",
{ refreshToken: "legacy-gemini-cli-refresh-old" },
{}
);
assert.deepEqual(calls, [], `expected zero upstream calls, got ${calls.join(", ")}`);
assert.notEqual(
calls[0],
OAUTH_ENDPOINTS.google.token,
"the Google token endpoint must not be contacted for a deprecated provider"
);
assert.equal(result.accessToken, undefined, "no token may be handed back");
assert.equal(result.code, "provider_deprecated");
} finally {
globalThis.fetch = originalFetch;
}
});
test("legacy Gemini CLI refresh reports deprecation, not a revoked token", async () => {
// Was: a 400 invalid_grant from upstream surfaced as
// { error: "unrecoverable_refresh_error", code: "invalid_grant" }. The envelope is
// deliberately unchanged — every existing caller keys on `error` and must keep
// stopping its retries (isUnrecoverableRefreshError, the manual-refresh route). Only
// the `code` differs, and that difference is the whole point: "your token was revoked"
// and "this provider no longer exists" demand different actions from the operator.
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({ error: "invalid_grant" }), {
status: 400,
headers: { "content-type": "application/json" },
})) as typeof fetch;
try {
const result = await getAccessToken(
"gemini-cli",
{ refreshToken: "legacy-gemini-cli-refresh-revoked" },
{}
);
assert.equal(result.error, "unrecoverable_refresh_error");
assert.equal(result.code, "provider_deprecated");
assert.equal(result.migrateTo, "gemini");
assert.match(result.reason, /gemini/i);
// The pre-deprecation behavior for a genuinely revoked token still works for the
// provider that IS routable — proof the deprecation did not blunt the real path.
const geminiResult = await getAccessToken("gemini", { refreshToken: "revoked" }, {});
assert.equal(geminiResult.error, "unrecoverable_refresh_error");
assert.equal(geminiResult.code, "invalid_grant");
} finally {
globalThis.fetch = originalFetch;
}
});