From 7d43db87726a2bb5def106ef64ad0d06a3098924 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 20 May 2026 23:21:59 -0500 Subject: [PATCH] test(opencode): use local server for Codex refresh dedupe --- packages/opencode/src/plugin/codex.ts | 18 ++- packages/opencode/test/plugin/codex.test.ts | 117 ++++++++++++-------- 2 files changed, 85 insertions(+), 50 deletions(-) diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 6679e5fe6b..d501e33652 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -118,6 +118,11 @@ interface TokenResponse { expires_in?: number } +interface CodexAuthPluginOptions { + issuer?: string + codexApiEndpoint?: string +} + async function exchangeCodeForTokens(code: string, redirectUri: string, pkce: PkceCodes): Promise { const response = await fetch(`${ISSUER}/oauth/token`, { method: "POST", @@ -136,8 +141,8 @@ async function exchangeCodeForTokens(code: string, redirectUri: string, pkce: Pk return response.json() } -async function refreshAccessToken(refreshToken: string): Promise { - const response = await fetch(`${ISSUER}/oauth/token`, { +async function refreshAccessToken(refreshToken: string, issuer = ISSUER): Promise { + const response = await fetch(`${issuer}/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ @@ -364,7 +369,10 @@ function waitForOAuthCallback(pkce: PkceCodes, state: string): Promise { +export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPluginOptions = {}): Promise { + const issuer = options.issuer ?? ISSUER + const codexApiEndpoint = options.codexApiEndpoint ?? CODEX_API_ENDPOINT + return { provider: { id: "openai", @@ -438,7 +446,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { if (!currentAuth.access || currentAuth.expires < Date.now()) { if (!refreshPromise) { log.info("refreshing codex access token") - refreshPromise = refreshAccessToken(currentAuth.refresh) + refreshPromise = refreshAccessToken(currentAuth.refresh, issuer) .then(async (tokens) => { const accountId = extractAccountId(tokens) || authWithAccount.accountId await input.client.auth.set({ @@ -497,7 +505,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { : new URL(typeof requestInput === "string" ? requestInput : requestInput.url) const url = parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions") - ? new URL(CODEX_API_ENDPOINT) + ? new URL(codexApiEndpoint) : parsed return fetch(url, { diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index 298539bb15..271bcde99b 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, mock, test } from "bun:test" +import { describe, expect, test } from "bun:test" import { CodexAuthPlugin, parseJwtClaims, @@ -7,12 +7,6 @@ import { type IdTokenClaims, } from "../../src/plugin/codex" -const originalFetch = globalThis.fetch - -afterEach(() => { - globalThis.fetch = originalFetch -}) - function createTestJwt(payload: object): string { const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url") const body = Buffer.from(JSON.stringify(payload)).toString("base64url") @@ -135,68 +129,101 @@ describe("plugin.codex", () => { access: "", expires: 0, } - const authUpdates: unknown[] = [] + const authUpdates: Array<{ + body: { refresh: string; access: string; expires: number; accountId?: string } + }> = [] let resolveRefresh: (() => void) | undefined const refreshReady = new Promise((resolve) => { resolveRefresh = resolve }) let refreshRequests = 0 + const apiRequests: { authorization: string | null; accountId: string | null }[] = [] - globalThis.fetch = mock(async (request: RequestInfo | URL) => { - const url = request instanceof URL ? request.href : typeof request === "string" ? request : request.url - if (url === "https://auth.openai.com/oauth/token") { - refreshRequests += 1 - await refreshReady - return new Response( - JSON.stringify({ + using server = Bun.serve({ + port: 0, + async fetch(request) { + const url = new URL(request.url) + if (url.pathname === "/oauth/token") { + expect(await request.text()).toContain("refresh_token=refresh-old") + refreshRequests += 1 + await refreshReady + return Response.json({ id_token: createTestJwt({ chatgpt_account_id: "acc-123" }), access_token: "access-new", refresh_token: "refresh-new", expires_in: 3600, - }), - { status: 200 }, - ) - } + }) + } - return new Response("{}", { status: 200 }) - }) as unknown as typeof fetch + if (url.pathname === "/backend-api/codex/responses") { + apiRequests.push({ + authorization: request.headers.get("authorization"), + accountId: request.headers.get("ChatGPT-Account-Id"), + }) + return new Response("{}", { status: 200 }) + } - const hooks = await CodexAuthPlugin({ - client: { - auth: { - async set(input: { body: { refresh: string; access: string; expires: number } }) { - authUpdates.push(input) - auth = { - type: "oauth", - refresh: input.body.refresh, - access: input.body.access, - expires: input.body.expires, - } - }, - }, - } as never, - project: {} as never, - directory: "", - worktree: "", - experimental_workspace: { - register() {}, + return new Response("unexpected request", { status: 500 }) }, - serverUrl: new URL("https://example.com"), - $: {} as never, }) + + const hooks = await CodexAuthPlugin( + { + client: { + auth: { + async set(input: { body: { refresh: string; access: string; expires: number; accountId?: string } }) { + authUpdates.push(input) + auth = { + type: "oauth", + refresh: input.body.refresh, + access: input.body.access, + expires: input.body.expires, + ...(input.body.accountId && { accountId: input.body.accountId }), + } + }, + }, + } as never, + project: {} as never, + directory: "", + worktree: "", + experimental_workspace: { + register() {}, + }, + serverUrl: new URL("https://example.com"), + $: {} as never, + }, + { + issuer: server.url.origin, + codexApiEndpoint: new URL("/backend-api/codex/responses", server.url).toString(), + }, + ) const loaded = await hooks.auth!.loader!(async () => auth as never, {} as never) const first = loaded.fetch!("https://api.openai.com/v1/responses") const second = loaded.fetch!("https://api.openai.com/v1/responses") - await Promise.resolve() - await Promise.resolve() - expect(refreshRequests).toBe(1) + await waitFor(() => refreshRequests === 1) + expect(apiRequests).toHaveLength(0) resolveRefresh!() await Promise.all([first, second]) expect(refreshRequests).toBe(1) expect(authUpdates).toHaveLength(1) + expect(authUpdates[0]?.body.refresh).toBe("refresh-new") + expect(authUpdates[0]?.body.access).toBe("access-new") + expect(authUpdates[0]?.body.accountId).toBe("acc-123") + expect(apiRequests).toEqual([ + { authorization: "Bearer access-new", accountId: "acc-123" }, + { authorization: "Bearer access-new", accountId: "acc-123" }, + ]) }) }) + +async function waitFor(predicate: () => boolean) { + const started = Date.now() + while (!predicate()) { + if (Date.now() - started > 1_000) throw new Error("timed out waiting for condition") + await new Promise((resolve) => setTimeout(resolve, 1)) + } +}