import { expect, test } from "vite-plus/test"; import { mkdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises"; import { codexAuthentication, isClaudeAuthFailure, isCodexAuthFailure, persistCodexAuth, retryInvalidToken, stageCodexAuth, } from "./auth.ts"; import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "./testing.ts"; test("retries once after an invalid refresh token", async () => { let attempts = 0; let relogins = 0; const result = await retryInvalidToken( () => Promise.resolve({ code: 1, error: attempts++ === 0 ? "ERROR: invalid_refresh_token" : "still failed", }), () => { relogins++; return Promise.resolve(); }, ); expect({ attempts, relogins, error: result.error }).toStrictEqual({ attempts: 2, relogins: 1, error: "still failed", }); }); test("does not retry an unrelated failure", async () => { let attempts = 0; let relogins = 0; await retryInvalidToken( () => { attempts++; return Promise.resolve({ code: 1, error: "rate limited", }); }, () => { relogins++; return Promise.resolve(); }, ); expect({ attempts, relogins }).toStrictEqual({ attempts: 1, relogins: 0 }); }); // Verbatim stderr lines from the revoked-login job on lead/memona-home#36. const REVOKED_CODEX = [ `2026-10-12T02:28:04.702126Z ERROR codex_models_manager::manager: failed to refresh available models: unexpected status 401 Unauthorized: Encountered invalidated oauth token for user, failing request, url: https://chatgpt.com/backend-api/codex/models?client_version=0.162.1, cf-ray: a492ab48fdefea25-ICN, request id: req_4caa104fe9184a63bb8877f42817172d, auth error: 401, auth error code: token_revoked`, `2026-10-12T02:28:05.082447Z ERROR codex_login::auth::manager: Failed to refresh token status=401 Unauthorized detail=TokenErrorDetail { error_code: Some("refresh_token_invalidated"), error_message: Some("Your session has ended. Please log in again."), .. }`, `2026-10-12T02:28:05.451678Z ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when UnexpectedServerResponse("HTTP 401: {\\n \\"error\\": {\\n \\"message\\": \\"Encountered invalidated oauth token for user, failing request\\",\\n \\"type\\": null,\\n \\"code\\": \\"token_revoked\\",\\n \\"param\\": null\\n },\\n \\"status\\": 401\\n}")`, "ERROR: workspace routing discovery unauthorized (401)", ]; test("recognizes each Codex sign-in failure line", () => { for (const line of [ ...REVOKED_CODEX, `2026-10-12T02:28:05Z ERROR codex_login::auth::manager: Failed to refresh token status=401 Unauthorized detail=TokenErrorDetail { error_code: Some("refresh_token_expired"), .. }`, `ERROR: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again. (refresh_token_reused)`, "ERROR: unexpected status 401 Unauthorized: Incorrect API key provided: sk-inval*d123., auth error: 401, auth error code: invalid_api_key", ]) { expect(isCodexAuthFailure(`exec\n${line}\n`), line).toBe(true); } }); test("ignores auth words outside Codex error lines", () => { expect( isCodexAuthFailure( "exec\ncurl returned 401 Unauthorized\ntoken_revoked in fixture\nERROR: tests failed\n", ), ).toBe(false); }); test("re-logs in after the job 9837 revoked-session stderr", async () => { let relogins = 0; let attempts = 0; const result = await retryInvalidToken( () => Promise.resolve({ code: attempts++ > 0 ? 0 : 1, error: attempts === 1 ? REVOKED_CODEX.join("\n") : "", }), () => { relogins++; return Promise.resolve(); }, ); expect({ relogins, code: result.code }).toStrictEqual({ relogins: 1, code: 0, }); }); test("recognizes a rejected Claude OAuth token", () => { expect( isClaudeAuthFailure({ api_error_status: 401, result: "Failed to authenticate. API Error: 401 OAuth access token is invalid.", }), ).toBe(true); expect( isClaudeAuthFailure({ result: "OAuth token has expired. Please obtain a new token or refresh your existing token.", }), ).toBe(true); expect(isClaudeAuthFailure({ api_error_status: 429, result: "API Error: 429" })).toBe(false); }); test("selects API-key login only for a configured Codex token", () => { expect(codexAuthentication("selected-key")).toStrictEqual({ kind: "api-key", token: "selected-key", }); expect(codexAuthentication()).toStrictEqual({ kind: "persistent" }); }); const AUTH_TOKENS = { access_token: "access", refresh_token: "refresh", id_token: "id", account_id: "account", }; const AUTH = JSON.stringify({ auth_mode: "chatgpt", tokens: AUTH_TOKENS }); test.skipIf(IS_WINDOWS)("stages and persists only a regular private Codex login", async () => { const root = await makeTempDir(); try { const canonical = `${root}/persistent/auth.json`; await mkdir(`${root}/persistent`); await writeFile(canonical, AUTH, { mode: 0o600 }); const home = `${root}/profile`; await mkdir(home); const staged = await stageCodexAuth(canonical, home, currentUid(), currentGid()); expect(await readFile(staged, "utf8")).toStrictEqual(AUTH); expect((await stat(staged)).mode & 0o777).toBe(0o600); const refreshed = JSON.stringify({ auth_mode: "chatgpt", tokens: { ...AUTH_TOKENS, access_token: "refreshed" }, }); await writeFile(staged, refreshed); await persistCodexAuth(staged, canonical); expect(await readFile(canonical, "utf8")).toStrictEqual(refreshed); expect((await stat(canonical)).mode & 0o777).toBe(0o600); } finally { await rm(root, { recursive: true }); } }); test.skipIf(IS_WINDOWS)("rejects a symlink or malformed Codex login", async () => { const root = await makeTempDir(); try { const canonical = `${root}/auth.json`; const malformed = `${root}/malformed.json`; const linked = `${root}/linked.json`; await writeFile(canonical, AUTH, { mode: 0o600 }); await writeFile(malformed, '{"tokens":{}}', { mode: 0o600 }); await symlink(canonical, linked); await expect(() => persistCodexAuth(linked, canonical)).rejects.toThrow("regular file"); await expect(() => persistCodexAuth(malformed, canonical)).rejects.toThrow( "invalid Codex login", ); } finally { await rm(root, { recursive: true }); } });