Skip to content

Commit faa97cb

Browse files
committed
fix: clarify manual callback fallback messaging
1 parent 93f62f5 commit faa97cb

2 files changed

Lines changed: 105 additions & 5 deletions

File tree

lib/codex-manager.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,7 +1511,8 @@ async function runOAuthFlow(
15111511
);
15121512
}
15131513

1514-
if (!preferManualMode && oauthServer?.ready) {
1514+
const waitingForCallback = !preferManualMode && oauthServer?.ready === true;
1515+
if (waitingForCallback && oauthServer) {
15151516
console.log(stylePromptText(UI_COPY.oauth.waitingCallback, "muted"));
15161517
const callbackResult = await oauthServer.waitForCode(state);
15171518
code = callbackResult?.code ?? null;
@@ -1520,7 +1521,9 @@ async function runOAuthFlow(
15201521
if (!code) {
15211522
console.log(
15221523
stylePromptText(
1523-
oauthServer?.ready ? UI_COPY.oauth.callbackMissed : UI_COPY.oauth.callbackUnavailable,
1524+
waitingForCallback
1525+
? UI_COPY.oauth.callbackMissed
1526+
: UI_COPY.oauth.callbackUnavailable,
15241527
"warning",
15251528
),
15261529
);

test/codex-manager-cli.test.ts

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2953,11 +2953,12 @@ describe("codex manager cli commands", () => {
29532953
expect(withAccountStorageTransactionMock).toHaveBeenCalledTimes(1);
29542954
expect(storageState.accounts).toHaveLength(2);
29552955
expect(storageState.activeIndex).toBe(1);
2956-
expect(storageState.activeIndexByFamily.codex).toBe(1);
2957-
expect(setCodexCliActiveSelectionMock).toHaveBeenCalledTimes(1);
2958-
});
2956+
expect(storageState.activeIndexByFamily.codex).toBe(1);
2957+
expect(setCodexCliActiveSelectionMock).toHaveBeenCalledTimes(1);
2958+
});
29592959

29602960
it("supports --manual login without launching a browser", async () => {
2961+
setInteractiveTTY(true);
29612962
const now = Date.now();
29622963
let storageState = {
29632964
version: 3 as const,
@@ -2999,13 +3000,19 @@ describe("codex manager cli commands", () => {
29993000
promptQuestionMock.mockResolvedValueOnce(
30003001
"http://127.0.0.1:1455/auth/callback?code=oauth-code&state=oauth-state",
30013002
);
3003+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
30023004

30033005
const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js");
30043006
const exitCode = await runCodexMultiAuthCli(["auth", "login", "--manual"]);
3007+
const renderedLogs = logSpy.mock.calls.flat().map((entry) => String(entry));
30053008

30063009
expect(exitCode).toBe(0);
30073010
expect(openBrowserUrlMock).not.toHaveBeenCalled();
30083011
expect(waitForCodeMock).not.toHaveBeenCalled();
3012+
expect(renderedLogs.some((entry) => entry.includes("Callback listener unavailable"))).toBe(
3013+
true,
3014+
);
3015+
expect(renderedLogs.some((entry) => entry.includes("No callback received"))).toBe(false);
30093016
expect(storageState.accounts).toHaveLength(1);
30103017
});
30113018

@@ -3058,6 +3065,96 @@ describe("codex manager cli commands", () => {
30583065
expect(storageState.accounts).toHaveLength(1);
30593066
});
30603067

3068+
it("rejects mismatched manual callback state in non-tty mode without persisting login", async () => {
3069+
setInteractiveTTY(false);
3070+
let storageState = {
3071+
version: 3 as const,
3072+
activeIndex: 0,
3073+
activeIndexByFamily: { codex: 0 },
3074+
accounts: [] as Array<Record<string, unknown>>,
3075+
};
3076+
loadAccountsMock.mockImplementation(async () => structuredClone(storageState));
3077+
saveAccountsMock.mockImplementation(async (nextStorage) => {
3078+
storageState = structuredClone(nextStorage);
3079+
});
3080+
promptLoginModeMock.mockResolvedValueOnce({ mode: "cancel" });
3081+
promptQuestionMock.mockResolvedValueOnce(
3082+
"http://127.0.0.1:1455/auth/callback?code=oauth-code&state=wrong-state",
3083+
);
3084+
3085+
const authModule = await import("../lib/auth/auth.js");
3086+
vi.mocked(authModule.createAuthorizationFlow).mockResolvedValueOnce({
3087+
pkce: { challenge: "pkce-challenge", verifier: "pkce-verifier" },
3088+
state: "oauth-state",
3089+
url: "https://auth.openai.com/mock",
3090+
});
3091+
const exchangeAuthorizationCodeMock = vi.mocked(authModule.exchangeAuthorizationCode);
3092+
3093+
const browserModule = await import("../lib/auth/browser.js");
3094+
const openBrowserUrlMock = vi.mocked(browserModule.openBrowserUrl);
3095+
const serverModule = await import("../lib/auth/server.js");
3096+
vi.mocked(serverModule.startLocalOAuthServer).mockRejectedValueOnce(
3097+
new Error("port in use"),
3098+
);
3099+
3100+
const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js");
3101+
const exitCode = await runCodexMultiAuthCli(["auth", "login", "--manual"]);
3102+
3103+
expect(exitCode).toBe(0);
3104+
expect(promptQuestionMock).toHaveBeenCalledWith("");
3105+
expect(openBrowserUrlMock).not.toHaveBeenCalled();
3106+
expect(exchangeAuthorizationCodeMock).not.toHaveBeenCalled();
3107+
expect(storageState.accounts).toHaveLength(0);
3108+
});
3109+
3110+
it("falls back to pasted manual input when Windows-style callback bind fails", async () => {
3111+
setInteractiveTTY(false);
3112+
const now = Date.now();
3113+
let storageState = {
3114+
version: 3 as const,
3115+
activeIndex: 0,
3116+
activeIndexByFamily: { codex: 0 },
3117+
accounts: [] as Array<Record<string, unknown>>,
3118+
};
3119+
loadAccountsMock.mockImplementation(async () => structuredClone(storageState));
3120+
saveAccountsMock.mockImplementation(async (nextStorage) => {
3121+
storageState = structuredClone(nextStorage);
3122+
});
3123+
promptLoginModeMock.mockResolvedValueOnce({ mode: "cancel" });
3124+
promptQuestionMock.mockResolvedValueOnce(
3125+
"http://127.0.0.1:1455/auth/callback?code=oauth-code&state=oauth-state",
3126+
);
3127+
3128+
const authModule = await import("../lib/auth/auth.js");
3129+
vi.mocked(authModule.createAuthorizationFlow).mockResolvedValueOnce({
3130+
pkce: { challenge: "pkce-challenge", verifier: "pkce-verifier" },
3131+
state: "oauth-state",
3132+
url: "https://auth.openai.com/mock",
3133+
});
3134+
vi.mocked(authModule.exchangeAuthorizationCode).mockResolvedValueOnce({
3135+
type: "success",
3136+
access: "access-eacces",
3137+
refresh: "refresh-eacces",
3138+
expires: now + 7_200_000,
3139+
idToken: "id-token-eacces",
3140+
multiAccount: true,
3141+
});
3142+
3143+
const browserModule = await import("../lib/auth/browser.js");
3144+
const openBrowserUrlMock = vi.mocked(browserModule.openBrowserUrl);
3145+
const serverModule = await import("../lib/auth/server.js");
3146+
vi.mocked(serverModule.startLocalOAuthServer).mockRejectedValueOnce(
3147+
Object.assign(new Error("permission denied"), { code: "EACCES" }),
3148+
);
3149+
3150+
const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js");
3151+
const exitCode = await runCodexMultiAuthCli(["auth", "login", "--manual"]);
3152+
3153+
expect(exitCode).toBe(0);
3154+
expect(promptQuestionMock).toHaveBeenCalledWith("");
3155+
expect(openBrowserUrlMock).not.toHaveBeenCalled();
3156+
expect(storageState.accounts).toHaveLength(1);
3157+
});
30613158
it("preserves distinct same-email workspaces when oauth login reuses a refresh token", async () => {
30623159
const now = Date.now();
30633160
let storageState = {

0 commit comments

Comments
 (0)