Files
bot-agents/src/auth.test.ts
T
Danny Kim 50328caac5
Check / check (pull_request) Successful in 2m26s
Agents / agent (pull_request) Has been skipped
Drop Deno from the agents and spawn child processes directly
2026-10-16 23:26:28 +09:00

179 lines
6.3 KiB
TypeScript

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 });
}
});