Skip to content

Commit 06340e6

Browse files
committed
fix: respect interactive manual auth selection
1 parent faa97cb commit 06340e6

2 files changed

Lines changed: 113 additions & 1 deletion

File tree

lib/codex-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1511,7 +1511,7 @@ async function runOAuthFlow(
15111511
);
15121512
}
15131513

1514-
const waitingForCallback = !preferManualMode && oauthServer?.ready === true;
1514+
const waitingForCallback = signInMode === "browser" && oauthServer?.ready === true;
15151515
if (waitingForCallback && oauthServer) {
15161516
console.log(stylePromptText(UI_COPY.oauth.waitingCallback, "muted"));
15171517
const callbackResult = await oauthServer.waitForCode(state);

test/codex-manager-cli.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3016,6 +3016,118 @@ describe("codex manager cli commands", () => {
30163016
expect(storageState.accounts).toHaveLength(1);
30173017
});
30183018

3019+
it("supports interactive manual login selection without waiting for a callback", async () => {
3020+
setInteractiveTTY(true);
3021+
const now = Date.now();
3022+
let storageState = {
3023+
version: 3 as const,
3024+
activeIndex: 0,
3025+
activeIndexByFamily: { codex: 0 },
3026+
accounts: [] as Array<Record<string, unknown>>,
3027+
};
3028+
loadAccountsMock.mockImplementation(async () => structuredClone(storageState));
3029+
saveAccountsMock.mockImplementation(async (nextStorage) => {
3030+
storageState = structuredClone(nextStorage);
3031+
});
3032+
promptLoginModeMock.mockResolvedValueOnce({ mode: "cancel" });
3033+
promptAddAnotherAccountMock.mockResolvedValue(false);
3034+
selectMock.mockResolvedValueOnce("manual");
3035+
3036+
const authModule = await import("../lib/auth/auth.js");
3037+
vi.mocked(authModule.createAuthorizationFlow).mockResolvedValueOnce({
3038+
pkce: { challenge: "pkce-challenge", verifier: "pkce-verifier" },
3039+
state: "oauth-state",
3040+
url: "https://auth.openai.com/mock",
3041+
});
3042+
vi.mocked(authModule.exchangeAuthorizationCode).mockResolvedValueOnce({
3043+
type: "success",
3044+
access: "access-manual-choice",
3045+
refresh: "refresh-manual-choice",
3046+
expires: now + 7_200_000,
3047+
idToken: "id-token-manual-choice",
3048+
multiAccount: true,
3049+
});
3050+
3051+
const browserModule = await import("../lib/auth/browser.js");
3052+
const openBrowserUrlMock = vi.mocked(browserModule.openBrowserUrl);
3053+
const serverModule = await import("../lib/auth/server.js");
3054+
const waitForCodeMock = vi.fn(async () => ({ code: "oauth-code" }));
3055+
vi.mocked(serverModule.startLocalOAuthServer).mockResolvedValueOnce({
3056+
ready: true,
3057+
waitForCode: waitForCodeMock,
3058+
close: vi.fn(),
3059+
});
3060+
promptQuestionMock.mockResolvedValueOnce(
3061+
"http://127.0.0.1:1455/auth/callback?code=oauth-code&state=oauth-state",
3062+
);
3063+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
3064+
3065+
const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js");
3066+
const exitCode = await runCodexMultiAuthCli(["auth", "login"]);
3067+
const renderedLogs = logSpy.mock.calls.flat().map((entry) => String(entry));
3068+
3069+
expect(exitCode).toBe(0);
3070+
expect(selectMock).toHaveBeenCalled();
3071+
expect(openBrowserUrlMock).not.toHaveBeenCalled();
3072+
expect(waitForCodeMock).not.toHaveBeenCalled();
3073+
expect(renderedLogs.some((entry) => entry.includes("Callback listener unavailable"))).toBe(
3074+
true,
3075+
);
3076+
expect(renderedLogs.some((entry) => entry.includes("No callback received"))).toBe(false);
3077+
expect(storageState.accounts).toHaveLength(1);
3078+
});
3079+
3080+
it("falls back to pasted callback input when browser launch is suppressed", async () => {
3081+
setInteractiveTTY(true);
3082+
const now = Date.now();
3083+
let storageState = {
3084+
version: 3 as const,
3085+
activeIndex: 0,
3086+
activeIndexByFamily: { codex: 0 },
3087+
accounts: [] as Array<Record<string, unknown>>,
3088+
};
3089+
loadAccountsMock.mockImplementation(async () => structuredClone(storageState));
3090+
saveAccountsMock.mockImplementation(async (nextStorage) => {
3091+
storageState = structuredClone(nextStorage);
3092+
});
3093+
promptLoginModeMock.mockResolvedValueOnce({ mode: "cancel" });
3094+
promptAddAnotherAccountMock.mockResolvedValue(false);
3095+
promptQuestionMock.mockResolvedValueOnce(
3096+
"http://127.0.0.1:1455/auth/callback?code=oauth-code&state=oauth-state",
3097+
);
3098+
3099+
const authModule = await import("../lib/auth/auth.js");
3100+
vi.mocked(authModule.createAuthorizationFlow).mockResolvedValueOnce({
3101+
pkce: { challenge: "pkce-challenge", verifier: "pkce-verifier" },
3102+
state: "oauth-state",
3103+
url: "https://auth.openai.com/mock",
3104+
});
3105+
vi.mocked(authModule.exchangeAuthorizationCode).mockResolvedValueOnce({
3106+
type: "success",
3107+
access: "access-suppressed",
3108+
refresh: "refresh-suppressed",
3109+
expires: now + 7_200_000,
3110+
idToken: "id-token-suppressed",
3111+
multiAccount: true,
3112+
});
3113+
3114+
const browserModule = await import("../lib/auth/browser.js");
3115+
const openBrowserUrlMock = vi.mocked(browserModule.openBrowserUrl);
3116+
vi.mocked(browserModule.isBrowserLaunchSuppressed).mockReturnValueOnce(true);
3117+
const serverModule = await import("../lib/auth/server.js");
3118+
const startLocalOAuthServerMock = vi.mocked(serverModule.startLocalOAuthServer);
3119+
startLocalOAuthServerMock.mockRejectedValueOnce(new Error("suppressed browser mode"));
3120+
3121+
const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js");
3122+
const exitCode = await runCodexMultiAuthCli(["auth", "login"]);
3123+
3124+
expect(exitCode).toBe(0);
3125+
expect(promptQuestionMock).toHaveBeenCalled();
3126+
expect(openBrowserUrlMock).not.toHaveBeenCalled();
3127+
expect(startLocalOAuthServerMock).toHaveBeenCalledTimes(1);
3128+
expect(storageState.accounts).toHaveLength(1);
3129+
});
3130+
30193131
it("accepts manual callback input in non-tty mode when --manual is set", async () => {
30203132
setInteractiveTTY(false);
30213133
const now = Date.now();

0 commit comments

Comments
 (0)