Files
OmniRoute/tests/unit/token-refresh-service.test.ts
Diego Rodrigues de Sa e Souza 555b21d296 Release v3.8.37 (#5053)
* chore(release): open v3.8.37 development cycle

* chore(ci): harden release flow — ratchet decoupling, fast-path drift gates, build-scope guard, heap default (#5054)

Implements improvements 1-4 from the v3.8.36 release benchmark (_tasks/release-bench/v3.8.36/PLANO-MELHORIA.md):

1. Quality Ratchet decoupled from flaky coverage (ci.yml): the shard→coverage→ratchet
   chain meant a single flaky Coverage Shard SKIPPED the whole Quality Ratchet on the
   release PR (v3.8.36 #4854), so cycle drift only surfaced post-merge in #5029. The job
   now runs on !cancelled(); coverage download is continue-on-error and the ratchet runs
   --allow-missing, so the DETERMINISTIC gates (eslint/complexity/cognitive/duplication/
   codeql) stay blocking even when coverage is unavailable.

2. Fast-path drift gates (quality.yml PR→release): added check:complexity,
   check:cognitive-complexity, and a new lightweight check:pack-policy (pack-artifact
   unexpected-files check WITHOUT a build, via --policy-only) so drift + stray-tarball-file
   regressions are caught/rebaselined PER-PR instead of cascading onto the release PR.

3. Build heap default 4096→8192 MB (build-next-isolated.mjs): the clean graph peaks
   ~3.9 GB and brushed the old 4 GB ceiling; 8 GB gives headroom. Comment notes heap is
   NOT the fix for a poisoned scope (run check:build-scope instead).

4. check:build-scope gate (new): fails if .ts/.tsx/.js/.jsx files in the tsconfig scope
   exceed a threshold — catches worktrees/cruft leaking into the build scope (the v3.8.36
   OOM root cause: 355,215 vs 4,547 files) BEFORE it detonates next build. Wired into the
   fast-path.

* fix(auth): only trust forwarding headers from loopback TCP peers (#4689)

Integrated into release/v3.8.37 — loopback-gated forwarding headers (IP spoofing fix). Cherry-picked onto current release tip; ipUtils.test.ts 9/9 green.

* fix(codex): treat OAuth 401 as unrecoverable refresh failure (#4686)

Integrated into release/v3.8.37 — codex OAuth 401 treated as unrecoverable refresh. Cherry-picked onto release tip; token-refresh-service.test.ts 38/38 green.

* fix(translator): preserve reasoning_effort for non-Copilot Responses clients (#4688)

Integrated into release/v3.8.37 — preserve reasoning_effort for non-Copilot Responses clients. Cherry-picked onto release tip; tests 47/47 green.

* fix(translator): coerce tool descriptions to strings in OpenAI normalization (#4675)

Integrated into release/v3.8.37 — coerce tool descriptions to strings in OpenAI normalization. Cherry-picked onto release tip; tests 3/3 green.

* feat(sse): x-omniroute-strip-reasoning header to drop reasoning_content (#4678)

Integrated into release/v3.8.37 — x-omniroute-strip-reasoning header. Cherry-picked onto release tip (resolved chatCore.ts/headers.ts adjacency conflict, kept resolveCompressionHeader + isStripReasoningRequested); tests 8/8 green.

* fix(combo): flatten Anthropic tool messages + tool history to prevent upstream 503 (#4648)

Integrated into release/v3.8.37 — flattenToolHistory helper (combo anti-503). Cherry-picked onto release tip; tests 9/9 green.

* feat(headroom): proxy lifecycle management + dashboard UI (Docker sidecar supported) (#4649)

Integrated into release/v3.8.37 — headroom proxy lifecycle (status/start/stop, local-only + spawn-capable per Rules #15/#17). Cherry-picked onto release tip; lifecycle 7/7 + route-guard 43/43 + check:cycles green.

* feat(cli): multi-model support for Factory Droid CLI (#4682)

Integrated into release/v3.8.37 — Factory Droid multi-model support. Cherry-picked onto release tip (kept readJsoncConfig + droidCustomModels imports); droid-custom-models 11/11 green.

* fix(providers): require Default Model in compatible-provider API-key setup (#4641)

Integrated into release/v3.8.37 — require Default Model in compatible-provider API-key setup. Cherry-picked fix + test-move onto release tip (kept release providerSpecificData + QuotaScrapingFields; fixed moved-test import path; baseline rebaseline unneeded, 865<866); UI test 2/2 green.

* fix(dashboard): stop double-masking already-masked API key in list (E2E 3/9 regression) (#4671)

Integrated into release/v3.8.37 — render server-masked key verbatim (drop redundant maskKey call). Note: release's maskKey already guards '****' (since v3.8.34), so this is a safe simplification; added a contract test pinning the **** passthrough invariant (2/2 green, would fail against the pre-guard maskKey = the historical double-mask bug).

* chore(quality): rebaseline file-size for rc17 PR batch drift

Own growth from the merged rc17 PRs (#4678/#4686/#4688) at existing chokepoints —
cohesive, not extractable:
- open-sse/handlers/responseSanitizer.ts 1103->1122 (SanitizeOpenAIResponseOptions + stripReasoning, #4678)
- open-sse/services/tokenRefresh.ts 2070->2090 (codex 401 unrecoverable-refresh guard, #4686)
- tests/unit/token-refresh-service.test.ts 1322->1353 (401 regression case, #4686)
- tests/unit/translator-openai-responses-req.test.ts 1047->1050 (reasoning_effort assertion, #4688)

* docs(env): document HEADROOM_URL in .env.example + ENVIRONMENT.md

The headroom proxy lifecycle (#4649) reads HEADROOM_URL (src/lib/headroom/detect.ts,
default http://localhost:8787) but it was missing from the env contract, tripping
check:env-doc-sync. Adds the var to both .env.example (commented, has a default) and
the Proxy Health table in ENVIRONMENT.md.

* fix(sse): stream writer mock abort() returns a Promise (#4788)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(cli): fall back to default data dir when DATA_DIR is not writable (#4767)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(oauth): verify Cursor installation on Linux before auto-import (#4770)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): track Ollama streaming usage from raw NDJSON chunks (#4754)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): strip enumDescriptions from antigravity tool schema (#4740)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): include low-level cause details in formatProviderError (#4741)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(translator): strip x-anthropic-billing-header in claude-to-openai (#4728)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): gate Kiro image attachments behind a Claude-capability check (#4763)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): read Antigravity usage from the response.usageMetadata envelope (#4785)

Integrated into release/v3.8.37 — Antigravity response.usageMetadata envelope. Cherry-picked onto release tip (resolved test-tail adjacency with #4754 Ollama block); usage-extractor 23/23 green.

* fix(api): fall back to existing access token for any OAuth provider on refresh failure (#4786)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(cli): verify launchd registration + skip self-SIGTERM in macOS autostart (#4765)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(executors): anthropic-compatible-* gateways get Bearer alongside x-api-key (#4729)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): json_schema fallback for OpenAI-compatible providers (#4766)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): use workos auth token shape for cline (#4787)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* feat(sse): parse Gemini CLI 429 retryDelay from structured RetryInfo (#4738)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): finalize tool_calls finish_reason on early stream end in OpenAI Responses translator (#4764)

Integrated into release/v3.8.37 — computeFinishReason finalizes tool_calls on early stream end (Responses translator). Cherry-picked onto release tip; responses-translation-fixes 29/29 green.

* test(sse): golden-lock provider.ts translate-path across all providers (#4734)

Integrated into release/v3.8.37 — golden-lock for provider.ts translate-path. Cherry-picked onto release tip; snapshot regenerated against the current provider set (UPDATE_GOLDEN=1, 167 entries); golden test 3/3 deterministic.

* chore(quality): rebaseline file-size for rc17 leva2 PR batch drift

Own growth from the merged leva2 PRs (cohesive, not extractable):
- src/lib/usage/providerLimits.ts 950->955 (#4786)
- open-sse/executors/default.ts NEW frozen @828 (#4729 + #4766 + #4787 header branches)
- open-sse/translator/request/openai-to-kiro.ts 807->814 (#4763)
- open-sse/translator/response/openai-responses.ts 923->937 (#4764)
- tests/unit/executor-default-base.test.ts 1339->1440 (#4766)
- tests/unit/translator-openai-to-kiro.test.ts 918->980 (#4763)

* fix(dashboard): align Engine Combos editor engines with API schema (#4955) (#5062)

The named-combos pipeline dropdown offered four engines (headroom,
session-dedup, ccr, llmlingua) that stackedPipelineStepSchema rejects, so
selecting one made PUT /api/context/combos/[id] return HTTP 400 while
saveCombo swallowed the non-OK response (if (!res.ok) return). Editing the
default 'Standard Savings' combo and changing an engine reproduced the 400.

- Add canonical STACKED_PIPELINE_ENGINE_INTENSITIES next to the schema as the
  single source of truth; the client dropdown imports it so it can never drift
  from the discriminated union the API validates against.
- Surface save errors and empty-name/empty-pipeline validation in the editor
  instead of failing silently.
- Add a parity unit test asserting the UI engine map equals the schema union
  and that every (engine, intensity) the UI emits is accepted.

* fix(sse): filter nameless hosted tools when converting Responses API to Chat format (#4789)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(dashboard): keep desktop sidebar visible via explicit CSS class (#4812)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip enumDescriptions from Antigravity tool schemas (#4813)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(dashboard): resolve passthrough model aliases by providerId in ModelSelectModal (#4815)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(oauth): allow per-connection refresh lead-time override via providerSpecificData.refreshLeadMs (#4818)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip X-Stainless-* headers and normalize SDK User-Agent for OpenAI-compatible endpoints (#4820)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip Gemini built-in tools when functionDeclarations present in Antigravity envelope (#4821)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(api): surface a Docker-localhost hint on provider-node validation connection errors (#4822)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): resolve bare model names to connection defaultModel before upstream calls (#4825)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(build): trace-include sql.js sql-wasm.wasm in standalone bundle (#4839)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip Composer <|final|> sentinel markers leaking after Composer reasoning (#4842)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(config): sync full SiliconFlow model list into registry (#4844)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): close reasoning before message content in Responses stream (#4848)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): reject unsupported Kiro [1m] context suffix (#4816)

Integrated into release/v3.8.37 — cherry-picked onto release tip; test-tail conflict with #4763 resolved (kept both image + [1m] test blocks); CHANGELOG re-merged; 29/29 green.

* fix(db): validate HuggingFace tokens via whoami-v2 auth probe (#4819)

Integrated into release/v3.8.37 — defining commit re-homed onto the god-file-split validation module (validateHuggingFaceProvider in validation/openaiFormat.ts + map wiring); 115/115 green.

* fix(sse): make anthropic-version default-guard case-insensitive (#4823)

Integrated into release/v3.8.37 — conflict with #4729 Bearer-fallback resolved (kept both Bearer fallback + case-insensitive anthropic-version guard); 48/48 green.

* fix(sse): sanitize Kiro tool schemas to avoid 400 "Improperly formed request" (#4847)

Integrated into release/v3.8.37 — conflict in kiro-to-openai.ts resolved (kept release fallbackToolCallId + adopted #1375 toolNameMap remap); 7/7 green.

* feat(sse): add GPT-4 to the GitHub Copilot provider (#4798)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(sse): add GPT-4o mini to GitHub Copilot provider (#4797)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(api): add MiniMax-M3 pricing row (#4814)

Integrated into release/v3.8.37 — pricing row re-homed onto god-file-split pricing/regional.ts (pricing.ts is now a barrel); 4/4 green.

* fix(cli): save runtime deps with --save-exact so a sibling install can't prune them (#4841)

Integrated into release/v3.8.37 — trayRuntime conflict resolved (kept release SYSTRAY_SPEC + added --save-exact); 2/2 green.

* fix(sse): preserve required fields in antigravity tool schemas (#4843)

Integrated into release/v3.8.37 — conflict resolved (kept #4740/#4813 enumDescriptions strip + typed normalizeSchemaTypes, added required-preservation helpers; test-tail merged keeping both enumDescriptions + required tests); 7/7 green.

* chore(quality): rebaseline file-size for rc17b leva3 PR batch drift

* fix(sse): strip reasoning blobs from agentic context to prevent O(n^2) token growth (#4849)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): unwrap Qoder HTTP 200 SSE error envelope so fallback can trigger (#4850)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip temperature for Claude models with extended thinking (#4853)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): emit valid concatenable kiro tool_calls.arguments deltas (#4855)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(sse): add toggleable tool-source diagnostics (#4856)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): redact api key from the AUTH debug log in the chat handler (#4858)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): forward AI SDK image parts in Responses translator (#4859)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): resolve custom combos by id and case-insensitive name (#4446) (#4869)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): exclude WS bridge controller-closed error from provider breaker (#4602) (#4870)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(providers): add xAI Grok inbound translators and thinking patcher (#4910)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(embeddings): add dimensions override field to embedding combos (#4913)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(oauth): Codex bulk-import endpoint — POST /api/oauth/codex/import (#4914)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(antigravity): retry transient upstream failures (#4941)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): surface malformed HTTP-200 upstream responses (#4942)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): normalize Codex custom tools (apply_patch) to { input: string } schema (#4862)

Integrated into release/v3.8.37 — conflict in request/openai-responses.ts resolved (kept #4789 nameless-tool skip + added #1007 custom-tool {input:string} normalization); 48/48 green incl. #4789/#4859 regression.

* fix(sse): dense, deterministic output ordering in Responses API response.completed (#4906)

Integrated into release/v3.8.37 — manual integration with #4862 in response/openai-responses.ts (custom-tool funcItem + dense recordCompletedItem). Fixed a latent #4848 interaction: the close-reasoning-before-message guard force-closed <think>-tag reasoning prematurely, which dense output (#4906) then snapshotted as a partial buffer ("plan" vs "planning") — scoped the guard to native reasoning_content (!inThinking) in BOTH transformer + translator paths. Full Responses suite 203/203 green incl. #4848/#4862 regression.

* feat(sse): auto-promote successful combo model to position #1 (#4852)

Integrated into release/v3.8.37 — dropped the stale file-size-baseline.json hunk (re-derived against the rc17b rebaseline); code+test applied clean; 13/13 green.

* feat(providers): add Pioneer AI (Fastino Labs) provider (#4909)

Integrated into release/v3.8.37 — providers.ts apikey block re-homed onto god-file-split src/shared/constants/providers/apikey/frontier-labs.ts (inline APIKEY_PROVIDERS no longer exists); registry/pioneer + providers/index.ts applied clean; 6/6 green.

* add DGrid AI gateway provider (#4931)

Integrated into release/v3.8.37 — rebased the contributor's commit onto the release tip; providers.ts god-file-split conflict resolved by relocating the dgrid APIKEY_PROVIDERS entry into apikey/gateways.ts; CHANGELOG added. 7/7 green. Thanks @dgridOP!

* chore(quality): rebaseline file-size for rc17b leva4 PR batch drift

* docs(routing): sync combo strategy docs for Fusion (17 strategies) (#5067)

Fusion (16th strategy, panel fan-out + judge synthesis) and headroom
shipped but the strategy-count docs were stale (14/15) and omitted both.
Update every combo-strategy reference to the canonical 17, add fusion +
headroom to all strategy tables, and add a dedicated Fusion section to
AUTO-COMBO.md documenting judgeModel / fusionTuning config + an example.

- CLAUDE.md, README.md, FEATURES.md, RESILIENCE_GUIDE.md,
  ARCHITECTURE.md, OPEN_SSE_ARCHITECTURE.md, OMNIROUTE_VS_ALTERNATIVES.md,
  docs/README.md, request-pipeline.mmd: 14/15 -> 17, list fusion + headroom
- docs/routing/AUTO-COMBO.md: strategy table + new Fusion strategy section
- docs/openapi.yaml: add reset-window, headroom, fusion to the strategy enum

* fix(oauth): classify /api/oauth/cursor/auto-import as local-only (route-guard) (#5070)

The Cursor auto-import route runs execFile("which", ["cursor"]) to verify a
local Cursor install before importing credentials — a child-process spawn. The
check:route-guard-membership gate (Hard Rules #15/#17) flagged it as an
unclassified spawn-capable route: reachable past the loopback gate, an
RCE-via-tunnel surface (a leaked JWT over a tunnel could trigger the spawn).

Classify the specific path in LOCAL_ONLY_API_PREFIXES so loopback enforcement
runs unconditionally before any auth check. Scoped to the exact path — the rest
of /api/oauth/ (browser redirect/callback flows) stays remote-reachable.

TDD: added a failing-then-passing assertion in route-guard-local-prefix.test.ts
(classification + an over-broadening guard proving sibling OAuth paths stay
remote). check:route-guard-membership now reports 0 new gaps.

* chore(release): v3.8.37 — 2026-06-26

---------

Co-authored-by: dgridOP <dgrid_op@outlook.com>
2026-06-26 02:51:06 -03:00

1353 lines
42 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const tokenRefresh = await import("../../open-sse/services/tokenRefresh.ts");
const { PROVIDERS, OAUTH_ENDPOINTS } = await import("../../open-sse/config/constants.ts");
const {
TOKEN_EXPIRY_BUFFER_MS,
refreshAccessToken,
refreshClineToken,
refreshKimiCodingToken,
refreshClaudeOAuthToken,
refreshGoogleToken,
refreshQwenToken,
refreshCodexToken,
refreshKiroToken,
refreshQoderToken,
refreshGitHubToken,
refreshCopilotToken,
supportsTokenRefresh,
isUnrecoverableRefreshError,
getAccessToken,
formatProviderCredentials,
getAllAccessTokens,
isProviderBlocked,
getCircuitBreakerStatus,
getConnectionRefreshMutexStatus,
refreshWithRetry,
} = tokenRefresh;
type LogLevel = "debug" | "info" | "warn" | "error";
type LogEntry = {
level: LogLevel;
scope: unknown;
message: unknown;
meta: unknown;
};
type MockLogger = {
entries: LogEntry[];
debug: (...args: [unknown?, unknown?, unknown?]) => void;
info: (...args: [unknown?, unknown?, unknown?]) => void;
warn: (...args: [unknown?, unknown?, unknown?]) => void;
error: (...args: [unknown?, unknown?, unknown?]) => void;
};
type TestFetch = typeof fetch;
type FastSetTimeout = typeof globalThis.setTimeout & {
__promisify__?: typeof globalThis.setTimeout.__promisify__;
};
function createLog(): MockLogger {
const entries: LogEntry[] = [];
const push = (level: LogLevel, args: [unknown?, unknown?, unknown?]) => {
const [scope, message, meta] = args;
entries.push({ level, scope, message, meta });
};
return {
entries,
debug: (...args) => push("debug", args),
info: (...args) => push("info", args),
warn: (...args) => push("warn", args),
error: (...args) => push("error", args),
};
}
function jsonResponse(body: any, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
function textResponse(text: any, status = 400) {
return new Response(text, {
status,
headers: { "content-type": "text/plain" },
});
}
function bodyToString(body: BodyInit | null | undefined) {
if (typeof body === "string") return body;
if (body instanceof URLSearchParams) return body.toString();
return String(body ?? "");
}
async function withMockedFetch<TResult>(fetchImpl: TestFetch, fn: () => Promise<TResult>) {
const originalFetch = globalThis.fetch;
globalThis.fetch = fetchImpl;
try {
return await fn();
} finally {
globalThis.fetch = originalFetch;
}
}
async function withMockedNow<TResult>(now: number, fn: () => Promise<TResult>) {
const originalNow = Date.now;
Date.now = () => now;
try {
return await fn();
} finally {
Date.now = originalNow;
}
}
async function withPatchedProperties<TResult>(
target: object,
patch: Record<string, unknown>,
fn: () => Promise<TResult>
) {
const previous = new Map<string, unknown>();
const targetRecord = target as Record<string, unknown>;
for (const [key, value] of Object.entries(patch)) {
previous.set(
key,
Object.prototype.hasOwnProperty.call(targetRecord, key) ? targetRecord[key] : undefined
);
targetRecord[key] = value;
}
try {
return await fn();
} finally {
for (const [key] of Object.entries(patch)) {
const prior = previous.get(key);
if (prior === undefined) {
delete targetRecord[key];
} else {
targetRecord[key] = prior;
}
}
}
}
async function withFastRetryTimers<TResult>(fn: () => Promise<TResult>) {
const originalSetTimeout = globalThis.setTimeout as FastSetTimeout;
const fastSetTimeout: FastSetTimeout = Object.assign(
((callback: TimerHandler, delay = 0, ...args: unknown[]) =>
originalSetTimeout(callback, delay === 30_000 ? delay : 0, ...args)) as typeof setTimeout,
{ __promisify__: originalSetTimeout.__promisify__ }
);
globalThis.setTimeout = fastSetTimeout;
try {
return await fn();
} finally {
globalThis.setTimeout = originalSetTimeout;
}
}
test("TOKEN_EXPIRY_BUFFER_MS stays at five minutes", () => {
assert.equal(TOKEN_EXPIRY_BUFFER_MS, 5 * 60 * 1000);
});
test("refreshAccessToken returns null when no provider refresh endpoint exists", async () => {
const log = createLog();
const result = await refreshAccessToken("qoder", "refresh-token", {}, log);
assert.equal(result, null);
assert.equal(
log.entries.some((entry) => entry.level === "warn"),
true
);
});
test("refreshAccessToken returns null when refresh token is missing", async () => {
const log = createLog();
await withPatchedProperties(
PROVIDERS,
{
"custom-oauth-task-207": { tokenUrl: "https://auth.example.com/token" },
},
async () => {
const result = await refreshAccessToken("custom-oauth-task-207", null, {}, log);
assert.equal(result, null);
}
);
});
test("refreshAccessToken posts form data and returns rotated tokens", async () => {
const log = createLog();
const calls: any[] = [];
await withPatchedProperties(
PROVIDERS,
{
"custom-oauth-task-207": {
refreshUrl: "https://auth.example.com/token",
clientId: "client-id",
clientSecret: "client-secret",
},
},
async () => {
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
access_token: "new-access",
refresh_token: "new-refresh",
expires_in: 3600,
});
},
async () => {
const result = await refreshAccessToken("custom-oauth-task-207", "refresh-123", {}, log);
assert.deepEqual(result, {
accessToken: "new-access",
refreshToken: "new-refresh",
expiresIn: 3600,
});
}
);
}
);
assert.equal(calls[0].url, "https://auth.example.com/token");
assert.equal(
bodyToString(calls[0].options.body),
"grant_type=refresh_token&refresh_token=refresh-123&client_id=client-id&client_secret=client-secret"
);
});
test("refreshAccessToken returns null on upstream refresh failure", async () => {
const log = createLog();
await withPatchedProperties(
PROVIDERS,
{
"custom-oauth-task-207": { tokenUrl: "https://auth.example.com/token" },
},
async () => {
await withMockedFetch(
async () => textResponse("rate limited", 429),
async () => {
const result = await refreshAccessToken("custom-oauth-task-207", "refresh-123", {}, log);
assert.equal(result, null);
assert.equal(
log.entries.some((entry) => entry.level === "error"),
true
);
}
);
}
);
});
test("refreshClineToken handles nested payloads and computes expiresIn", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedNow(1_700_000_000_000, async () => {
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
data: {
accessToken: "cline-access",
refreshToken: "cline-refresh",
expiresAt: new Date(Date.now() + 95_000).toISOString(),
},
});
},
async () => {
const result = await refreshClineToken("refresh-cline", log);
assert.equal(result?.accessToken, "cline-access");
assert.equal(result?.refreshToken, "cline-refresh");
assert.equal(result?.expiresIn, 95);
}
);
});
assert.equal(calls[0].url, PROVIDERS.cline.refreshUrl);
assert.deepEqual(JSON.parse(calls[0].options.body), {
refreshToken: "refresh-cline",
grantType: "refresh_token",
clientType: "extension",
});
});
test("refreshKimiCodingToken adds provider-specific headers and fields", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
access_token: "kimi-access",
refresh_token: "kimi-refresh-next",
expires_in: 7200,
token_type: "Bearer",
scope: "coding offline_access",
});
},
async () => {
// Pass providerSpecificData with a stable deviceId (second positional arg after signature change)
const result = await refreshKimiCodingToken(
"kimi-refresh",
{ deviceId: "test-stable-device" },
log
);
assert.deepEqual(result, {
accessToken: "kimi-access",
refreshToken: "kimi-refresh-next",
expiresIn: 7200,
tokenType: "Bearer",
scope: "coding offline_access",
});
}
);
assert.equal(calls[0].url, PROVIDERS["kimi-coding"].refreshUrl);
// Platform is now "kimi_cli" (matching the real Kimi CLI fingerprint)
assert.equal(calls[0].options.headers["X-Msh-Platform"], "kimi_cli");
// Version comes from KIMI_CLI_VERSION env or default "1.36.0"
assert.ok(calls[0].options.headers["X-Msh-Version"], "X-Msh-Version must be set");
// Device-Id must NOT be an ephemeral "kimi-refresh-<timestamp>" value
assert.ok(
calls[0].options.headers["X-Msh-Device-Id"] &&
!calls[0].options.headers["X-Msh-Device-Id"].startsWith("kimi-refresh-"),
"X-Msh-Device-Id must be stable (not ephemeral kimi-refresh-<timestamp>)"
);
// When providerSpecificData.deviceId is provided, it should be used directly
assert.equal(calls[0].options.headers["X-Msh-Device-Id"], "test-stable-device");
assert.match(bodyToString(calls[0].options.body), /grant_type=refresh_token/);
});
test("refreshClaudeOAuthToken posts the anthropic oauth refresh contract", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
access_token: "claude-access",
refresh_token: "claude-refresh-next",
expires_in: 1800,
});
},
async () => {
const result = await refreshClaudeOAuthToken("claude-refresh", log);
assert.deepEqual(result, {
accessToken: "claude-access",
refreshToken: "claude-refresh-next",
expiresIn: 1800,
});
}
);
assert.equal(calls[0].url, OAUTH_ENDPOINTS.anthropic.token);
assert.equal(calls[0].options.headers["anthropic-beta"], "oauth-2025-04-20");
assert.match(calls[0].options.body, /grant_type=refresh_token/);
assert.match(calls[0].options.body, /client_id=/);
});
test("refreshGoogleToken exchanges refresh tokens against the shared google endpoint", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
access_token: "google-access",
refresh_token: "google-refresh-next",
expires_in: 3600,
});
},
async () => {
const result = await refreshGoogleToken("google-refresh", "gid", "gsecret", log);
assert.deepEqual(result, {
accessToken: "google-access",
refreshToken: "google-refresh-next",
expiresIn: 3600,
});
}
);
assert.equal(calls[0].url, OAUTH_ENDPOINTS.google.token);
assert.equal(
bodyToString(calls[0].options.body),
"grant_type=refresh_token&refresh_token=google-refresh&client_id=gid&client_secret=gsecret"
);
});
test("refreshQwenToken maps resource_url into providerSpecificData", async () => {
const log = createLog();
await withMockedFetch(
async () =>
jsonResponse({
access_token: "qwen-access",
refresh_token: "qwen-refresh-next",
expires_in: 1500,
resource_url: "https://chat.qwen.ai/workspace/resource",
}),
async () => {
const result = await refreshQwenToken("qwen-refresh", log);
assert.deepEqual(result, {
accessToken: "qwen-access",
refreshToken: "qwen-refresh-next",
expiresIn: 1500,
providerSpecificData: {
resourceUrl: "https://chat.qwen.ai/workspace/resource",
},
});
}
);
});
test("refreshQwenToken surfaces invalid_request as unrecoverable", async () => {
const log = createLog();
await withMockedFetch(
async () => textResponse(JSON.stringify({ error: "invalid_request" }), 400),
async () => {
const result = await refreshQwenToken("qwen-refresh", log);
// Normalized to unrecoverable_refresh_error sentinel (Fix 4)
assert.deepEqual(result, { error: "unrecoverable_refresh_error", code: "invalid_request" });
}
);
});
test("refreshCodexToken recognizes refresh_token_reused responses", async () => {
const log = createLog();
await withMockedFetch(
async () => textResponse(JSON.stringify({ error: { code: "refresh_token_reused" } }), 400),
async () => {
const result = await refreshCodexToken("codex-refresh", log);
assert.deepEqual(result, {
error: "unrecoverable_refresh_error",
code: "refresh_token_reused",
});
}
);
});
// Port from decolua/9router#1821 (sacwooky): a 401 from OpenAI's OAuth token
// endpoint means the refresh credential itself was rejected (e.g. rotated away
// or a payload whose error code we do not yet recognize). Retrying with the
// same refresh token will never succeed — surface re-auth, do not loop.
test("refreshCodexToken treats any 401 from the token endpoint as unrecoverable", async () => {
const log = createLog();
await withMockedFetch(
async () =>
textResponse(
JSON.stringify({
error: {
// A payload variant whose code/type are NOT in the existing
// unrecoverable set — only the 401 status proves the token is dead.
message: "Could not validate your token. Please try signing in again.",
type: "invalid_request_error",
},
}),
401
),
async () => {
const result = await refreshCodexToken("codex-refresh", log);
assert.equal(
result?.error,
"unrecoverable_refresh_error",
"401 from OpenAI token endpoint must surface re-auth instead of returning null (which triggers retry)"
);
}
);
});
test("refreshKiroToken uses the AWS OIDC flow when client credentials are present", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
accessToken: "kiro-aws-access",
refreshToken: "kiro-aws-refresh-next",
expiresIn: 900,
});
},
async () => {
const result = await refreshKiroToken(
"kiro-refresh",
{
authMethod: "idc",
clientId: "aws-client",
clientSecret: "aws-secret",
region: "eu-west-1",
},
log
);
assert.deepEqual(result, {
accessToken: "kiro-aws-access",
refreshToken: "kiro-aws-refresh-next",
expiresIn: 900,
});
}
);
assert.equal(calls[0].url, "https://oidc.eu-west-1.amazonaws.com/token");
assert.deepEqual(JSON.parse(calls[0].options.body), {
clientId: "aws-client",
clientSecret: "aws-secret",
refreshToken: "kiro-refresh",
grantType: "refresh_token",
});
});
test("refreshKiroToken uses stored region for AWS OIDC refresh without authMethod", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
accessToken: "kiro-aws-access",
refreshToken: "kiro-aws-refresh-next",
expiresIn: 900,
});
},
async () => {
const result = await refreshKiroToken(
"kiro-refresh",
{
clientId: "aws-client",
clientSecret: "aws-secret",
region: "ap-southeast-1",
},
log
);
assert.deepEqual(result, {
accessToken: "kiro-aws-access",
refreshToken: "kiro-aws-refresh-next",
expiresIn: 900,
});
}
);
assert.equal(calls[0].url, "https://oidc.ap-southeast-1.amazonaws.com/token");
assert.deepEqual(JSON.parse(calls[0].options.body), {
clientId: "aws-client",
clientSecret: "aws-secret",
refreshToken: "kiro-refresh",
grantType: "refresh_token",
});
});
test("refreshKiroToken falls back to the social-auth refresh endpoint", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
accessToken: "kiro-social-access",
refreshToken: "kiro-social-refresh-next",
expiresIn: 1200,
});
},
async () => {
const result = await refreshKiroToken("kiro-refresh", null, log);
assert.deepEqual(result, {
accessToken: "kiro-social-access",
refreshToken: "kiro-social-refresh-next",
expiresIn: 1200,
});
}
);
assert.equal(calls[0].url, PROVIDERS.kiro.tokenUrl);
assert.deepEqual(JSON.parse(calls[0].options.body), {
refreshToken: "kiro-refresh",
});
});
// Issue #2328 — once a social-auth token has clientId/clientSecret stored
// (because it was imported after v3.8.0), refreshKiroToken must use the AWS OIDC
// endpoint, not the shared social-auth endpoint, even though authMethod is "google".
test("refreshKiroToken uses AWS OIDC path for social-auth token when clientId is present (#2328)", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
accessToken: "kiro-isolated-access",
refreshToken: "kiro-isolated-refresh-next",
expiresIn: 900,
});
},
async () => {
const result = await refreshKiroToken(
"kiro-social-refresh",
{
authMethod: "google",
clientId: "isolated-client-id",
clientSecret: "isolated-client-secret",
region: "us-east-1",
},
log
);
assert.deepEqual(result, {
accessToken: "kiro-isolated-access",
refreshToken: "kiro-isolated-refresh-next",
expiresIn: 900,
});
}
);
// Must call the AWS OIDC endpoint — not the shared social-auth tokenUrl
assert.ok(
calls[0].url.includes("oidc.us-east-1.amazonaws.com/token"),
`expected AWS OIDC endpoint but got ${calls[0].url}`
);
assert.notEqual(
calls[0].url,
PROVIDERS.kiro.tokenUrl,
"should not call the shared social-auth endpoint when clientId is set"
);
assert.deepEqual(JSON.parse(calls[0].options.body), {
clientId: "isolated-client-id",
clientSecret: "isolated-client-secret",
refreshToken: "kiro-social-refresh",
grantType: "refresh_token",
});
});
// Issue #2467 — an IMPORTED social token (authMethod === "imported") carries a
// freshly-registered clientId/clientSecret, but its refresh token is Kiro-social-issued
// and the isolated OIDC client cannot refresh it. It must use the social-auth endpoint,
// NOT AWS OIDC (which is what #2328 enabled for authMethod "google").
test("refreshKiroToken uses social-auth path for imported token even with clientId (#2467)", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
accessToken: "kiro-imported-access",
refreshToken: "kiro-imported-refresh-next",
expiresIn: 1100,
});
},
async () => {
const result = await refreshKiroToken(
"kiro-imported-refresh",
{
authMethod: "imported",
clientId: "isolated-client-id",
clientSecret: "isolated-client-secret",
region: "us-east-1",
},
log
);
assert.equal(result.accessToken, "kiro-imported-access");
}
);
// Must call the shared social-auth tokenUrl — NOT the AWS OIDC endpoint.
assert.equal(
calls[0].url,
PROVIDERS.kiro.tokenUrl,
`expected social-auth endpoint but got ${calls[0].url}`
);
assert.ok(!calls[0].url.includes("oidc."), "imported token must not use AWS OIDC");
});
test("refreshQoderToken uses basic auth once qoder oauth settings are configured", async () => {
const log = createLog();
const calls: any[] = [];
await withPatchedProperties(
PROVIDERS.qoder,
{
clientId: "qoder-client",
clientSecret: "qoder-secret",
},
async () => {
await withPatchedProperties(
OAUTH_ENDPOINTS.qoder,
{
token: "https://qoder.example.com/oauth/token",
},
async () => {
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
access_token: "qoder-access",
refresh_token: "qoder-refresh-next",
expires_in: 2400,
});
},
async () => {
const result = await refreshQoderToken("qoder-refresh", log);
assert.deepEqual(result, {
accessToken: "qoder-access",
refreshToken: "qoder-refresh-next",
expiresIn: 2400,
});
}
);
}
);
}
);
assert.equal(calls[0].url, "https://qoder.example.com/oauth/token");
assert.match(calls[0].options.headers.Authorization, /^Basic /);
});
test("refreshGitHubToken sends the real public github client_id and no client_secret (port from 9router#442)", async () => {
// GitHub Copilot's OAuth app is a public device-flow client: it has a client_id but
// NO client_secret. PROVIDERS.github.clientId must be populated from the embedded public
// cred so the refresh request actually carries a client_id — a missing one makes GitHub
// reject the refresh. The previous test patched a fake clientId/clientSecret onto
// PROVIDERS.github, masking the fact that the real config had neither. This uses the real
// config and asserts the real client_id is sent and no client_secret leaks out.
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
access_token: "github-access",
refresh_token: "github-refresh-next",
expires_in: 3600,
});
},
async () => {
const result = await refreshGitHubToken("github-refresh", log);
assert.deepEqual(result, {
accessToken: "github-access",
refreshToken: "github-refresh-next",
expiresIn: 3600,
});
}
);
const body = bodyToString(calls[0].options.body);
assert.equal(calls[0].url, OAUTH_ENDPOINTS.github.token);
assert.ok(PROVIDERS.github.clientId, "PROVIDERS.github.clientId must be populated from the public cred");
assert.match(body, /client_id=Iv1\./, "the real public github client_id must be sent on refresh");
assert.ok(!body.includes("client_secret="), "no client_secret for the public github client");
});
test("refreshCopilotToken returns the short-lived copilot token", async () => {
const log = createLog();
const calls: any[] = [];
await withMockedFetch(
async (url, options = {}) => {
calls.push({ url, options });
return jsonResponse({
token: "copilot-session-token",
expires_at: "2026-01-01T00:00:00.000Z",
});
},
async () => {
const result = await refreshCopilotToken("github-access-token", log);
assert.deepEqual(result, {
token: "copilot-session-token",
expiresAt: "2026-01-01T00:00:00.000Z",
});
}
);
assert.equal(calls[0].url, "https://api.github.com/copilot_internal/v2/token");
assert.equal(calls[0].options.headers.Authorization, "token github-access-token");
});
test("supportsTokenRefresh, isUnrecoverableRefreshError and formatProviderCredentials cover provider helpers", async () => {
const log = createLog();
await withPatchedProperties(
PROVIDERS,
{
"custom-oauth-task-207": { tokenUrl: "https://auth.example.com/token" },
},
async () => {
assert.equal(supportsTokenRefresh("claude"), true);
assert.equal(supportsTokenRefresh("amazon-q"), true);
assert.equal(supportsTokenRefresh("custom-oauth-task-207"), true);
assert.equal(supportsTokenRefresh("missing-provider"), false);
}
);
assert.equal(isUnrecoverableRefreshError({ error: "refresh_token_reused" }), true);
assert.equal(isUnrecoverableRefreshError({ error: "invalid_request" }), true);
assert.equal(isUnrecoverableRefreshError({ error: "temporary_failure" }), false);
assert.deepEqual(
formatProviderCredentials(
"gemini",
{
apiKey: "gemini-key",
accessToken: "gemini-access",
projectId: "project-1",
refreshToken: "ignored",
},
log
),
{
apiKey: "gemini-key",
accessToken: "gemini-access",
projectId: "project-1",
}
);
assert.deepEqual(
formatProviderCredentials(
"antigravity",
{
accessToken: "google-access",
refreshToken: "google-refresh",
},
log
),
{
accessToken: "google-access",
refreshToken: "google-refresh",
}
);
assert.equal(formatProviderCredentials("missing-provider", {}, log), null);
});
test("getAccessToken deduplicates concurrent refreshes for the same provider and token", async () => {
const log = createLog();
let fetchCount = 0;
await withPatchedProperties(
PROVIDERS,
{
"custom-oauth-task-207": { tokenUrl: "https://auth.example.com/token" },
},
async () => {
await withMockedFetch(
async () => {
fetchCount += 1;
return jsonResponse({
access_token: "shared-access",
refresh_token: "shared-refresh-next",
expires_in: 600,
});
},
async () => {
const [first, second] = await Promise.all([
getAccessToken("custom-oauth-task-207", { refreshToken: "same-refresh" }, log),
getAccessToken("custom-oauth-task-207", { refreshToken: "same-refresh" }, log),
]);
assert.equal(fetchCount, 1);
assert.strictEqual(first, second);
assert.equal(first.accessToken, "shared-access");
}
);
}
);
});
test("getAccessToken cleans the in-flight cache after resolve and separates different tokens", async () => {
const log = createLog();
let fetchCount = 0;
await withPatchedProperties(
PROVIDERS,
{
"custom-oauth-task-207": { tokenUrl: "https://auth.example.com/token" },
},
async () => {
await withMockedFetch(
async (_url, options: RequestInit = {}) => {
fetchCount += 1;
const refreshToken = new URLSearchParams(bodyToString(options.body)).get("refresh_token");
return jsonResponse({
access_token: `access-${refreshToken}`,
refresh_token: `next-${refreshToken}`,
expires_in: 600,
});
},
async () => {
const first = await getAccessToken(
"custom-oauth-task-207",
{ refreshToken: "refresh-a" },
log
);
const second = await getAccessToken(
"custom-oauth-task-207",
{ refreshToken: "refresh-a" },
log
);
const third = await getAccessToken(
"custom-oauth-task-207",
{ refreshToken: "refresh-b" },
log
);
assert.equal(fetchCount, 3);
assert.equal(first.accessToken, "access-refresh-a");
assert.equal(second.accessToken, "access-refresh-a");
assert.equal(third.accessToken, "access-refresh-b");
}
);
}
);
});
test("getAccessToken returns null for invalid refresh token input", async () => {
const log = createLog();
const result = await getAccessToken("codex", { refreshToken: null }, log);
assert.equal(result, null);
});
test("getAllAccessTokens refreshes only active connections with providers", async () => {
const log = createLog();
let fetchCount = 0;
await withPatchedProperties(
PROVIDERS,
{
"custom-oauth-task-207": { tokenUrl: "https://auth.example.com/token" },
},
async () => {
await withMockedFetch(
async (_url, options: RequestInit = {}) => {
fetchCount += 1;
const refreshToken = new URLSearchParams(bodyToString(options.body)).get("refresh_token");
return jsonResponse({
access_token: `access-${refreshToken}`,
refresh_token: `next-${refreshToken}`,
expires_in: 900,
});
},
async () => {
const tokens = await getAllAccessTokens(
{
connections: [
{
provider: "custom-oauth-task-207",
refreshToken: "active-one",
isActive: true,
},
{
provider: "custom-oauth-task-207",
refreshToken: "inactive-one",
isActive: false,
},
{
provider: null,
refreshToken: "missing-provider",
isActive: true,
},
],
},
log
);
assert.equal(fetchCount, 1);
assert.deepEqual(tokens, {
"custom-oauth-task-207": {
accessToken: "access-active-one",
refreshToken: "next-active-one",
expiresIn: 900,
},
});
}
);
}
);
});
test("refreshWithRetry retries to success and clears prior circuit-breaker state", async () => {
const provider = `retry-success-${Date.now()}`;
const log = createLog();
await refreshWithRetry(async () => null, 1, log, provider);
assert.equal(getCircuitBreakerStatus()[provider].failures, 1);
await withFastRetryTimers(async () => {
let attempts = 0;
const result = await refreshWithRetry(
async () => {
attempts += 1;
return attempts === 2 ? { accessToken: "recovered" } : null;
},
3,
log,
provider
);
assert.deepEqual(result, { accessToken: "recovered" });
assert.equal(attempts, 2);
assert.equal(getCircuitBreakerStatus()[provider], undefined);
});
});
test("refreshWithRetry trips the circuit breaker after repeated failures and blocks new calls", async () => {
const provider = `retry-blocked-${Date.now()}`;
const log = createLog();
for (let attempt = 0; attempt < 5; attempt += 1) {
const result = await refreshWithRetry(async () => null, 1, log, provider);
assert.equal(result, null);
}
assert.equal(isProviderBlocked(provider), true);
assert.equal(getCircuitBreakerStatus()[provider].blocked, true);
let called = false;
const blockedResult = await refreshWithRetry(
async () => {
called = true;
return { accessToken: "should-not-run" };
},
1,
log,
provider
);
assert.equal(blockedResult, null);
assert.equal(called, false);
});
test("isProviderBlocked clears expired circuit-breaker entries once cooldown passes", async () => {
const provider = `retry-expiry-${Date.now()}`;
const log = createLog();
for (let attempt = 0; attempt < 5; attempt += 1) {
await refreshWithRetry(async () => null, 1, log, provider);
}
const blockedUntil = Date.parse(getCircuitBreakerStatus()[provider].blockedUntil as string);
await withMockedNow(blockedUntil + 1, async () => {
assert.equal(isProviderBlocked(provider), false);
assert.equal(getCircuitBreakerStatus()[provider], undefined);
});
});
// ─── Per-connection mutex tests ────────────────────────────────────────────────
test("getAccessToken per-connection mutex: 5 concurrent callers fire exactly one upstream call", async () => {
const log = createLog();
let upstreamCallCount = 0;
await withPatchedProperties(
PROVIDERS,
{ "custom-oauth-conn-mutex": { tokenUrl: "https://auth.example.com/token" } },
async () => {
await withMockedFetch(
async () => {
upstreamCallCount++;
// Simulate 50ms upstream latency so all 5 callers are concurrent
await new Promise((r) => setTimeout(r, 50));
return jsonResponse({
access_token: "new-access-token",
refresh_token: "new-refresh-token",
expires_in: 3600,
});
},
async () => {
const credentials = {
connectionId: "conn-abc-123",
refreshToken: "old-refresh-token",
};
const results = await Promise.all([
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
]);
assert.equal(upstreamCallCount, 1, "upstream called exactly once");
for (const result of results) {
assert.equal(result?.accessToken, "new-access-token", "all callers got same token");
assert.equal(result?.refreshToken, "new-refresh-token");
}
// All results are the same object reference (shared promise)
assert.strictEqual(results[0], results[1]);
assert.strictEqual(results[1], results[4]);
}
);
}
);
});
test("getAccessToken per-connection mutex: logs concurrent refresh with waiter count", async () => {
const log = createLog();
await withPatchedProperties(
PROVIDERS,
{ "custom-oauth-conn-mutex": { tokenUrl: "https://auth.example.com/token" } },
async () => {
await withMockedFetch(
async () => {
await new Promise((r) => setTimeout(r, 20));
return jsonResponse({ access_token: "tok", refresh_token: "rtok", expires_in: 600 });
},
async () => {
const credentials = { connectionId: "conn-log-test", refreshToken: "rt" };
await Promise.all([
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
]);
const concurrentLogs = log.entries.filter(
(e) =>
e.level === "info" &&
e.message === "Concurrent refresh detected — sharing in-flight refresh"
);
assert.ok(concurrentLogs.length >= 1, "logged at least one concurrent refresh event");
assert.ok(
concurrentLogs.some((e) => e.meta?.connectionId === "conn-log-test"),
"log includes connectionId"
);
assert.ok(
concurrentLogs.some((e) => typeof e.meta?.waiters === "number" && e.meta.waiters >= 1),
"log includes waiter count"
);
}
);
}
);
});
test("getAccessToken per-connection mutex: failed refresh propagates null to all waiters (idempotent error)", async () => {
const log = createLog();
await withPatchedProperties(
PROVIDERS,
{ "custom-oauth-conn-mutex": { tokenUrl: "https://auth.example.com/token" } },
async () => {
await withMockedFetch(
async () => {
await new Promise((r) => setTimeout(r, 20));
// 400 response causes refreshAccessToken to return null
return new Response("bad_request", { status: 400 });
},
async () => {
const credentials = { connectionId: "conn-fail-test", refreshToken: "expired-rt" };
const results = await Promise.all([
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log),
]);
for (const result of results) {
assert.equal(result, null, "failed refresh returns null to all waiters");
}
// Mutex cleaned up after failure
assert.equal(
getConnectionRefreshMutexStatus()["conn-fail-test"],
undefined,
"mutex entry removed after failure"
);
}
);
}
);
});
test("getAccessToken per-connection mutex: different connections run independently", async () => {
const log = createLog();
let upstreamCallCount = 0;
await withPatchedProperties(
PROVIDERS,
{ "custom-oauth-conn-mutex": { tokenUrl: "https://auth.example.com/token" } },
async () => {
await withMockedFetch(
async () => {
upstreamCallCount++;
await new Promise((r) => setTimeout(r, 20));
return jsonResponse({
access_token: `access-${upstreamCallCount}`,
refresh_token: `refresh-${upstreamCallCount}`,
expires_in: 600,
});
},
async () => {
const [groupA, groupB] = await Promise.all([
Promise.all([
getAccessToken(
"custom-oauth-conn-mutex",
{ connectionId: "conn-A", refreshToken: "rt-a" },
log
),
getAccessToken(
"custom-oauth-conn-mutex",
{ connectionId: "conn-A", refreshToken: "rt-a" },
log
),
]),
Promise.all([
getAccessToken(
"custom-oauth-conn-mutex",
{ connectionId: "conn-B", refreshToken: "rt-b" },
log
),
getAccessToken(
"custom-oauth-conn-mutex",
{ connectionId: "conn-B", refreshToken: "rt-b" },
log
),
]),
]);
assert.equal(upstreamCallCount, 2, "one upstream call per distinct connection");
assert.strictEqual(groupA[0], groupA[1], "conn-A callers share same result");
assert.strictEqual(groupB[0], groupB[1], "conn-B callers share same result");
assert.notStrictEqual(groupA[0], groupB[0], "conn-A and conn-B got different results");
}
);
}
);
});
test("getAccessToken per-connection mutex: mutex cleared after success, next call re-fires upstream", async () => {
const log = createLog();
let upstreamCallCount = 0;
// The rotation map (added for the codex-multi-auth pattern) is process-wide
// and intentionally redirects a stale-token caller to the cached rotated
// tokens. Clear it BEFORE and BETWEEN calls so this test exercises the
// lower-level mutex semantics it was designed for.
tokenRefresh._clearTokenRotationMap();
await withPatchedProperties(
PROVIDERS,
{ "custom-oauth-conn-mutex": { tokenUrl: "https://auth.example.com/token" } },
async () => {
await withMockedFetch(
async () => {
upstreamCallCount++;
return jsonResponse({
access_token: `access-${upstreamCallCount}`,
refresh_token: `refresh-${upstreamCallCount}`,
expires_in: 600,
});
},
async () => {
const credentials = { connectionId: "conn-refire", refreshToken: "rt" };
const first = await getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log);
tokenRefresh._clearTokenRotationMap();
const second = await getAccessToken("custom-oauth-conn-mutex", { ...credentials }, log);
assert.equal(upstreamCallCount, 2, "each sequential call fires upstream once");
assert.equal(first?.accessToken, "access-1");
assert.equal(second?.accessToken, "access-2");
}
);
}
);
});
// ─── Unrecoverable error bail-out tests ──────────────────────────────────────
test("refreshWithRetry bails immediately on unrecoverable error without retrying", async () => {
const provider = `bail-unrecoverable-${Date.now()}`;
const log = createLog();
let callCount = 0;
const result = await refreshWithRetry(
async () => {
callCount++;
return { error: "unrecoverable_refresh_error", code: "http_400" };
},
3,
log,
provider
);
assert.equal(callCount, 1, "should only call refreshFn once (no retries)");
assert.deepEqual(result, { error: "unrecoverable_refresh_error", code: "http_400" });
const warnMessages = log.entries.filter((e) => e.level === "warn").map((e) => e.message);
assert.ok(
warnMessages.some((m) => String(m).includes("Unrecoverable")),
"should log an unrecoverable warning"
);
});
test("refreshWithRetry bails immediately on invalid_grant error without retrying", async () => {
const provider = `bail-invalid-grant-${Date.now()}`;
const log = createLog();
let callCount = 0;
const result = await refreshWithRetry(
async () => {
callCount++;
return { error: "invalid_grant", code: "http_400" };
},
3,
log,
provider
);
assert.equal(callCount, 1, "should only call refreshFn once (no retries)");
assert.deepEqual(result, { error: "invalid_grant", code: "http_400" });
});
test("refreshClaudeOAuthToken returns error object for invalid_grant (expired refresh token)", async () => {
const log = createLog();
await withMockedFetch(
async () =>
new Response(JSON.stringify({ error: "invalid_grant", error_description: "Token expired" }), {
status: 400,
headers: { "Content-Type": "application/json" },
}),
async () => {
const result = await refreshClaudeOAuthToken("expired-token", log);
assert.ok(result && typeof result === "object", "should return error object, not null");
// Normalized to unrecoverable_refresh_error sentinel (Fix 6)
assert.equal((result as any).error, "unrecoverable_refresh_error");
assert.equal((result as any).code, "invalid_grant");
assert.ok(isUnrecoverableRefreshError(result), "should be detected as unrecoverable");
}
);
});
test("refreshClaudeOAuthToken returns null for transient server errors (not unrecoverable)", async () => {
const log = createLog();
await withMockedFetch(
async () =>
new Response(JSON.stringify({ error: "server_error" }), {
status: 503,
headers: { "Content-Type": "application/json" },
}),
async () => {
const result = await refreshClaudeOAuthToken("some-token", log);
assert.equal(result, null, "transient server errors should return null (retryable)");
}
);
});