Skip to content

Commit 9d81368

Browse files
committed
fix: respect interactive manual auth selection
1 parent 0e0ffc8 commit 9d81368

2 files changed

Lines changed: 62 additions & 1 deletion

File tree

lib/codex-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1529,7 +1529,7 @@ async function runOAuthFlow(
15291529
);
15301530
}
15311531

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

test/codex-manager-cli.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3016,6 +3016,67 @@ 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+
30193080
it("falls back to pasted callback input when browser launch is suppressed", async () => {
30203081
setInteractiveTTY(true);
30213082
const now = Date.now();

0 commit comments

Comments
 (0)