From 4baddb0d5dc347c822fb8e1867f032cf512014b8 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sat, 14 Mar 2026 16:19:12 +0800 Subject: [PATCH 1/2] fix dashboard manage actions to use visible row numbers --- lib/cli.ts | 78 +++++++++++++-- lib/codex-manager.ts | 58 +++++++++-- test/cli-auth-menu.test.ts | 72 ++++++++++++-- test/codex-manager-cli.test.ts | 177 ++++++++++++++++++++++++++++++++- 4 files changed, 356 insertions(+), 29 deletions(-) diff --git a/lib/cli.ts b/lib/cli.ts index d223c14c..f223b43c 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -91,6 +91,7 @@ export interface LoginMenuResult { refreshAccountIndex?: number; toggleAccountIndex?: number; switchAccountIndex?: number; + selectedAccountNumber?: number; deleteAll?: boolean; } @@ -122,11 +123,40 @@ function resolveAccountSourceIndex(account: ExistingAccountInfo): number { return -1; } +function resolveAccountDisplayNumber( + account: ExistingAccountInfo, +): number | undefined { + if ( + typeof account.quickSwitchNumber === "number" && + Number.isFinite(account.quickSwitchNumber) + ) { + return Math.max(1, Math.floor(account.quickSwitchNumber)); + } + if (typeof account.index === "number" && Number.isFinite(account.index)) { + return Math.max(1, Math.floor(account.index) + 1); + } + return undefined; +} + function warnUnresolvableAccountSelection(account: ExistingAccountInfo): void { const label = account.email?.trim() || account.accountId?.trim() || `index ${account.index + 1}`; console.log(`Unable to resolve saved account for action: ${label}`); } +function buildManageResult( + account: ExistingAccountInfo, + result: Omit, +): LoginMenuResult { + const selectedAccountNumber = resolveAccountDisplayNumber(account); + return { + mode: "manage", + ...result, + ...(typeof selectedAccountNumber === "number" + ? { selectedAccountNumber } + : {}), + }; +} + async function promptDeleteAllTypedConfirm(): Promise { const rl = createInterface({ input, output }); try { @@ -225,25 +255,41 @@ export async function promptLoginMode( const accountAction = await showAccountDetails(action.account); if (accountAction === "delete") { const index = resolveAccountSourceIndex(action.account); - if (index >= 0) return { mode: "manage", deleteAccountIndex: index }; + if (index >= 0) { + return buildManageResult(action.account, { + deleteAccountIndex: index, + }); + } warnUnresolvableAccountSelection(action.account); continue; } if (accountAction === "set-current") { const index = resolveAccountSourceIndex(action.account); - if (index >= 0) return { mode: "manage", switchAccountIndex: index }; + if (index >= 0) { + return buildManageResult(action.account, { + switchAccountIndex: index, + }); + } warnUnresolvableAccountSelection(action.account); continue; } if (accountAction === "refresh") { const index = resolveAccountSourceIndex(action.account); - if (index >= 0) return { mode: "manage", refreshAccountIndex: index }; + if (index >= 0) { + return buildManageResult(action.account, { + refreshAccountIndex: index, + }); + } warnUnresolvableAccountSelection(action.account); continue; } if (accountAction === "toggle") { const index = resolveAccountSourceIndex(action.account); - if (index >= 0) return { mode: "manage", toggleAccountIndex: index }; + if (index >= 0) { + return buildManageResult(action.account, { + toggleAccountIndex: index, + }); + } warnUnresolvableAccountSelection(action.account); continue; } @@ -251,25 +297,41 @@ export async function promptLoginMode( } case "set-current-account": { const index = resolveAccountSourceIndex(action.account); - if (index >= 0) return { mode: "manage", switchAccountIndex: index }; + if (index >= 0) { + return buildManageResult(action.account, { + switchAccountIndex: index, + }); + } warnUnresolvableAccountSelection(action.account); continue; } case "refresh-account": { const index = resolveAccountSourceIndex(action.account); - if (index >= 0) return { mode: "manage", refreshAccountIndex: index }; + if (index >= 0) { + return buildManageResult(action.account, { + refreshAccountIndex: index, + }); + } warnUnresolvableAccountSelection(action.account); continue; } case "toggle-account": { const index = resolveAccountSourceIndex(action.account); - if (index >= 0) return { mode: "manage", toggleAccountIndex: index }; + if (index >= 0) { + return buildManageResult(action.account, { + toggleAccountIndex: index, + }); + } warnUnresolvableAccountSelection(action.account); continue; } case "delete-account": { const index = resolveAccountSourceIndex(action.account); - if (index >= 0) return { mode: "manage", deleteAccountIndex: index }; + if (index >= 0) { + return buildManageResult(action.account, { + deleteAccountIndex: index, + }); + } warnUnresolvableAccountSelection(action.account); continue; } diff --git a/lib/codex-manager.ts b/lib/codex-manager.ts index b3212c30..d23bc189 100644 --- a/lib/codex-manager.ts +++ b/lib/codex-manager.ts @@ -3736,18 +3736,44 @@ async function clearAccountsAndReset(): Promise { await clearAccounts(); } +function resolveManageActionAccountNumber( + menuResult: Awaited>, + fallbackIndex: number, +): number { + if ( + typeof menuResult.selectedAccountNumber === "number" && + Number.isFinite(menuResult.selectedAccountNumber) + ) { + return Math.max(1, Math.floor(menuResult.selectedAccountNumber)); + } + return fallbackIndex + 1; +} + +interface RunSwitchOptions { + displayAccountNumber?: number; +} + async function handleManageAction( storage: AccountStorageV3, menuResult: Awaited>, ): Promise { if (typeof menuResult.switchAccountIndex === "number") { const index = menuResult.switchAccountIndex; - await runSwitch([String(index + 1)]); + await runSwitch([String(index + 1)], { + displayAccountNumber: resolveManageActionAccountNumber( + menuResult, + index, + ), + }); return; } if (typeof menuResult.deleteAccountIndex === "number") { const idx = menuResult.deleteAccountIndex; + const displayAccountNumber = resolveManageActionAccountNumber( + menuResult, + idx, + ); if (idx >= 0 && idx < storage.accounts.length) { storage.accounts.splice(idx, 1); storage.activeIndex = 0; @@ -3756,19 +3782,23 @@ async function handleManageAction( storage.activeIndexByFamily[family] = 0; } await saveAccounts(storage); - console.log(`Deleted account ${idx + 1}.`); + console.log(`Deleted account ${displayAccountNumber}.`); } return; } if (typeof menuResult.toggleAccountIndex === "number") { const idx = menuResult.toggleAccountIndex; + const displayAccountNumber = resolveManageActionAccountNumber( + menuResult, + idx, + ); const account = storage.accounts[idx]; if (account) { account.enabled = account.enabled === false; await saveAccounts(storage); console.log( - `${account.enabled === false ? "Disabled" : "Enabled"} account ${idx + 1}.`, + `${account.enabled === false ? "Disabled" : "Enabled"} account ${displayAccountNumber}.`, ); } return; @@ -3776,6 +3806,10 @@ async function handleManageAction( if (typeof menuResult.refreshAccountIndex === "number") { const idx = menuResult.refreshAccountIndex; + const displayAccountNumber = resolveManageActionAccountNumber( + menuResult, + idx, + ); const existing = storage.accounts[idx]; if (!existing) return; @@ -3788,7 +3822,7 @@ async function handleManageAction( const resolved = resolveAccountSelection(tokenResult); await persistAccountPool([resolved], false); await syncSelectionToCodex(resolved); - console.log(`Refreshed account ${idx + 1}.`); + console.log(`Refreshed account ${displayAccountNumber}.`); } } @@ -3945,7 +3979,10 @@ async function runAuthLogin(): Promise { } } -async function runSwitch(args: string[]): Promise { +async function runSwitch( + args: string[], + options: RunSwitchOptions = {}, +): Promise { setStoragePath(null); const indexArg = args[0]; if (!indexArg) { @@ -3958,6 +3995,11 @@ async function runSwitch(args: string[]): Promise { return 1; } const targetIndex = parsed - 1; + const displayAccountNumber = + typeof options.displayAccountNumber === "number" && + Number.isFinite(options.displayAccountNumber) + ? Math.max(1, Math.floor(options.displayAccountNumber)) + : parsed; const storage = await loadAccounts(); if (!storage || storage.accounts.length === 0) { @@ -4017,7 +4059,7 @@ async function runSwitch(args: string[]): Promise { syncIdToken = refreshResult.idToken; } else { console.warn( - `Switch validation refresh failed for account ${parsed}: ${normalizeFailureDetail(refreshResult.message, refreshResult.reason)}.`, + `Switch validation refresh failed for account ${displayAccountNumber}: ${normalizeFailureDetail(refreshResult.message, refreshResult.reason)}.`, ); } } @@ -4036,12 +4078,12 @@ async function runSwitch(args: string[]): Promise { }); if (!synced) { console.warn( - `Switched account ${parsed} locally, but Codex auth sync did not complete. Multi-auth routing will still use this account.`, + `Switched account ${displayAccountNumber} locally, but Codex auth sync did not complete. Multi-auth routing will still use this account.`, ); } console.log( - `Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`, + `Switched to account ${displayAccountNumber}: ${formatAccountLabel(account, displayAccountNumber - 1)}${wasDisabled ? " (re-enabled)" : ""}`, ); return 0; } diff --git a/test/cli-auth-menu.test.ts b/test/cli-auth-menu.test.ts index 0f06f2c3..f35ee3ee 100644 --- a/test/cli-auth-menu.test.ts +++ b/test/cli-auth-menu.test.ts @@ -44,7 +44,11 @@ describe("CLI auth menu shortcuts", () => { const { promptLoginMode } = await import("../lib/cli.js"); const result = await promptLoginMode([{ index: 0 }, { index: 1 }]); - expect(result).toEqual({ mode: "manage", switchAccountIndex: 1 }); + expect(result).toEqual({ + mode: "manage", + switchAccountIndex: 1, + selectedAccountNumber: 2, + }); }); it("uses source index for set current when sorted view provides source mapping", async () => { @@ -56,7 +60,29 @@ describe("CLI auth menu shortcuts", () => { const { promptLoginMode } = await import("../lib/cli.js"); const result = await promptLoginMode([{ index: 0, sourceIndex: 4 }]); - expect(result).toEqual({ mode: "manage", switchAccountIndex: 4 }); + expect(result).toEqual({ + mode: "manage", + switchAccountIndex: 4, + selectedAccountNumber: 1, + }); + }); + + it("preserves visible row numbers separately from saved source indexes", async () => { + showAuthMenu.mockResolvedValueOnce({ + type: "set-current-account", + account: { index: 0, sourceIndex: 28, quickSwitchNumber: 1 }, + }); + + const { promptLoginMode } = await import("../lib/cli.js"); + const result = await promptLoginMode([ + { index: 0, sourceIndex: 28, quickSwitchNumber: 1 }, + ]); + + expect(result).toEqual({ + mode: "manage", + switchAccountIndex: 28, + selectedAccountNumber: 1, + }); }); it("returns switch action when account details picks set current", async () => { @@ -69,7 +95,11 @@ describe("CLI auth menu shortcuts", () => { const { promptLoginMode } = await import("../lib/cli.js"); const result = await promptLoginMode([{ index: 0 }, { index: 1 }, { index: 2 }]); - expect(result).toEqual({ mode: "manage", switchAccountIndex: 2 }); + expect(result).toEqual({ + mode: "manage", + switchAccountIndex: 2, + selectedAccountNumber: 3, + }); }); it("returns refresh action when auth menu requests refresh", async () => { @@ -81,7 +111,11 @@ describe("CLI auth menu shortcuts", () => { const { promptLoginMode } = await import("../lib/cli.js"); const result = await promptLoginMode([{ index: 0 }]); - expect(result).toEqual({ mode: "manage", refreshAccountIndex: 0 }); + expect(result).toEqual({ + mode: "manage", + refreshAccountIndex: 0, + selectedAccountNumber: 1, + }); }); it("returns toggle action when auth menu requests toggle", async () => { @@ -93,7 +127,11 @@ describe("CLI auth menu shortcuts", () => { const { promptLoginMode } = await import("../lib/cli.js"); const result = await promptLoginMode([{ index: 0 }, { index: 1 }]); - expect(result).toEqual({ mode: "manage", toggleAccountIndex: 1 }); + expect(result).toEqual({ + mode: "manage", + toggleAccountIndex: 1, + selectedAccountNumber: 2, + }); }); it("uses source index for account-details actions when sorted view provides source mapping", async () => { @@ -106,7 +144,11 @@ describe("CLI auth menu shortcuts", () => { const { promptLoginMode } = await import("../lib/cli.js"); const result = await promptLoginMode([{ index: 0, sourceIndex: 3 }]); - expect(result).toEqual({ mode: "manage", refreshAccountIndex: 3 }); + expect(result).toEqual({ + mode: "manage", + refreshAccountIndex: 3, + selectedAccountNumber: 1, + }); }); it("returns forecast mode when auth menu requests forecast", async () => { @@ -151,7 +193,11 @@ describe("CLI auth menu shortcuts", () => { const { promptLoginMode } = await import("../lib/cli.js"); const result = await promptLoginMode([{ index: 0 }]); - expect(result).toEqual({ mode: "manage", deleteAccountIndex: 0 }); + expect(result).toEqual({ + mode: "manage", + deleteAccountIndex: 0, + selectedAccountNumber: 1, + }); }); it("returns deep-check mode when auth menu requests deep-check", async () => { @@ -237,7 +283,11 @@ describe("CLI auth menu shortcuts", () => { const { promptLoginMode } = await import("../lib/cli.js"); const result = await promptLoginMode([{ index: 0 }, { index: 1 }]); - expect(result).toEqual({ mode: "manage", deleteAccountIndex: 1 }); + expect(result).toEqual({ + mode: "manage", + deleteAccountIndex: 1, + selectedAccountNumber: 2, + }); }); it("returns manage toggle action when account details picks toggle", async () => { @@ -250,7 +300,11 @@ describe("CLI auth menu shortcuts", () => { const { promptLoginMode } = await import("../lib/cli.js"); const result = await promptLoginMode([{ index: 0, sourceIndex: 2 }]); - expect(result).toEqual({ mode: "manage", toggleAccountIndex: 2 }); + expect(result).toEqual({ + mode: "manage", + toggleAccountIndex: 2, + selectedAccountNumber: 1, + }); }); it("continues when account-details delete cannot resolve source index", async () => { diff --git a/test/codex-manager-cli.test.ts b/test/codex-manager-cli.test.ts index 4340b503..9a99d9ba 100644 --- a/test/codex-manager-cli.test.ts +++ b/test/codex-manager-cli.test.ts @@ -1281,6 +1281,62 @@ describe("codex manager cli commands", () => { expect(logSpy).toHaveBeenCalledWith("Cancelled."); }); + it("uses visible dashboard row numbers when manage mode switches a reordered account", async () => { + const now = Date.now(); + const storage = { + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: Array.from({ length: 29 }, (_, index) => ({ + email: `account-${index + 1}@example.com`, + accountId: `acc_${index + 1}`, + refreshToken: `refresh-${index + 1}`, + accessToken: `access-${index + 1}`, + expiresAt: now + 3_600_000, + addedAt: now - 1_000 - index, + lastUsed: now - 1_000 - index, + enabled: true, + })), + }; + loadAccountsMock.mockResolvedValue(storage); + setCodexCliActiveSelectionMock.mockResolvedValue(true); + promptLoginModeMock + .mockResolvedValueOnce({ + mode: "manage", + switchAccountIndex: 28, + selectedAccountNumber: 1, + }) + .mockResolvedValueOnce({ mode: "cancel" }); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + expect(promptLoginModeMock).toHaveBeenCalledTimes(2); + expect(saveAccountsMock).toHaveBeenCalledTimes(1); + expect(saveAccountsMock.mock.calls[0]?.[0]?.activeIndex).toBe(28); + expect(saveAccountsMock.mock.calls[0]?.[0]?.activeIndexByFamily?.codex).toBe( + 28, + ); + expect(setCodexCliActiveSelectionMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: "account-29@example.com", + accountId: "acc_29", + refreshToken: "refresh-29", + }), + ); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining("Switched to account 1"), + ); + expect( + logSpy.mock.calls.some((call) => + String(call[0]).includes("Switched to account 29"), + ), + ).toBe(false); + expect(logSpy).toHaveBeenCalledWith("Cancelled."); + }); + it("marks newly added login account active so smart sort reflects it immediately", async () => { const now = Date.now(); let storageState: { @@ -3166,9 +3222,14 @@ describe("codex manager cli commands", () => { ], }); promptLoginModeMock - .mockResolvedValueOnce({ mode: "manage", deleteAccountIndex: 1 }) + .mockResolvedValueOnce({ + mode: "manage", + deleteAccountIndex: 1, + selectedAccountNumber: 1, + }) .mockResolvedValueOnce({ mode: "cancel" }); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); const exitCode = await runCodexMultiAuthCli(["auth", "login"]); @@ -3178,6 +3239,12 @@ describe("codex manager cli commands", () => { expect(saveAccountsMock.mock.calls[0]?.[0]?.accounts?.[0]?.email).toBe( "first@example.com", ); + expect(logSpy).toHaveBeenCalledWith("Deleted account 1."); + expect( + logSpy.mock.calls.some((call) => + String(call[0]).includes("Deleted account 2"), + ), + ).toBe(false); }); it("toggles account enabled state from manage mode", async () => { @@ -3188,26 +3255,128 @@ describe("codex manager cli commands", () => { activeIndexByFamily: { codex: 0 }, accounts: [ { - email: "toggle@example.com", - refreshToken: "refresh-toggle", + email: "first@example.com", + refreshToken: "refresh-first", addedAt: now - 1_000, lastUsed: now - 1_000, enabled: true, }, + { + email: "toggle@example.com", + refreshToken: "refresh-toggle", + addedAt: now - 500, + lastUsed: now - 500, + enabled: true, + }, ], }); promptLoginModeMock - .mockResolvedValueOnce({ mode: "manage", toggleAccountIndex: 0 }) + .mockResolvedValueOnce({ + mode: "manage", + toggleAccountIndex: 1, + selectedAccountNumber: 1, + }) .mockResolvedValueOnce({ mode: "cancel" }); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); const exitCode = await runCodexMultiAuthCli(["auth", "login"]); expect(exitCode).toBe(0); expect(saveAccountsMock).toHaveBeenCalledTimes(1); expect(saveAccountsMock.mock.calls[0]?.[0]?.accounts?.[0]?.enabled).toBe( + true, + ); + expect(saveAccountsMock.mock.calls[0]?.[0]?.accounts?.[1]?.enabled).toBe( false, ); + expect(logSpy).toHaveBeenCalledWith("Disabled account 1."); + expect( + logSpy.mock.calls.some((call) => + String(call[0]).includes("Disabled account 2"), + ), + ).toBe(false); + }); + + it("refreshes reordered accounts from manage mode using visible row numbers", async () => { + const now = Date.now(); + let storageState = { + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: [ + { + email: "first@example.com", + accountId: "acc_first", + refreshToken: "refresh-first", + accessToken: "access-first", + expiresAt: now + 3_600_000, + addedAt: now - 2_000, + lastUsed: now - 2_000, + enabled: true, + }, + { + email: "refresh@example.com", + accountId: "acc_refresh", + refreshToken: "refresh-second", + accessToken: "access-second", + expiresAt: now + 3_600_000, + addedAt: now - 1_000, + lastUsed: now - 1_000, + enabled: true, + }, + ], + }; + loadAccountsMock.mockImplementation(async () => + structuredClone(storageState), + ); + saveAccountsMock.mockImplementation(async (nextStorage) => { + storageState = structuredClone(nextStorage); + }); + promptLoginModeMock + .mockResolvedValueOnce({ + mode: "manage", + refreshAccountIndex: 1, + selectedAccountNumber: 1, + }) + .mockResolvedValueOnce({ mode: "cancel" }); + const authModule = await import("../lib/auth/auth.js"); + const browserModule = await import("../lib/auth/browser.js"); + const serverModule = await import("../lib/auth/server.js"); + vi.mocked(authModule.createAuthorizationFlow).mockResolvedValue({ + pkce: { challenge: "pkce-challenge", verifier: "pkce-verifier" }, + state: "oauth-state", + url: "https://auth.openai.com/mock", + }); + vi.mocked(authModule.exchangeAuthorizationCode).mockResolvedValue({ + type: "success", + access: "access-second-next", + refresh: "refresh-second-next", + expires: now + 7_200_000, + idToken: "id-second-next", + }); + vi.mocked(browserModule.openBrowserUrl).mockReturnValue(true); + vi.mocked(serverModule.startLocalOAuthServer).mockResolvedValue({ + ready: true, + waitForCode: vi.fn(async () => ({ code: "oauth-code" })), + close: vi.fn(), + }); + setCodexCliActiveSelectionMock.mockResolvedValue(true); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + expect(authModule.createAuthorizationFlow).toHaveBeenCalledTimes(1); + expect(authModule.exchangeAuthorizationCode).toHaveBeenCalledTimes(1); + expect(setCodexCliActiveSelectionMock).toHaveBeenCalledTimes(1); + expect(logSpy).toHaveBeenCalledWith("Refreshed account 1."); + expect( + logSpy.mock.calls.some((call) => + String(call[0]).includes("Refreshed account 2"), + ), + ).toBe(false); }); it("keeps settings unchanged in non-interactive mode and returns to menu", async () => { From d9556abaa6a09f2d1796c8ccef2251129a2dbfa1 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sat, 14 Mar 2026 19:07:51 +0800 Subject: [PATCH 2/2] fix stale manage switch error messaging --- lib/codex-manager.ts | 6 ++-- test/codex-manager-cli.test.ts | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/codex-manager.ts b/lib/codex-manager.ts index d23bc189..b7f1aff4 100644 --- a/lib/codex-manager.ts +++ b/lib/codex-manager.ts @@ -4007,13 +4007,15 @@ async function runSwitch( return 1; } if (targetIndex < 0 || targetIndex >= storage.accounts.length) { - console.error(`Index out of range. Valid range: 1-${storage.accounts.length}`); + console.error( + `Selected account ${displayAccountNumber} is out of range. Valid range: 1-${storage.accounts.length}.`, + ); return 1; } const account = storage.accounts[targetIndex]; if (!account) { - console.error(`Account ${parsed} not found.`); + console.error(`Selected account ${displayAccountNumber} is no longer available.`); return 1; } diff --git a/test/codex-manager-cli.test.ts b/test/codex-manager-cli.test.ts index 9a99d9ba..4b11beed 100644 --- a/test/codex-manager-cli.test.ts +++ b/test/codex-manager-cli.test.ts @@ -1337,6 +1337,61 @@ describe("codex manager cli commands", () => { expect(logSpy).toHaveBeenCalledWith("Cancelled."); }); + it("keeps stale manage switch errors on the visible dashboard row number", async () => { + const now = Date.now(); + const storage = { + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: Array.from({ length: 29 }, (_, index) => ({ + email: `account-${index + 1}@example.com`, + accountId: `acc_${index + 1}`, + refreshToken: `refresh-${index + 1}`, + accessToken: `access-${index + 1}`, + expiresAt: now + 3_600_000, + addedAt: now - 1_000 - index, + lastUsed: now - 1_000 - index, + enabled: true, + })), + }; + const staleAccounts = storage.accounts.slice(0, 28); + staleAccounts.length = 29; + let loadCall = 0; + loadAccountsMock.mockImplementation(async () => { + loadCall += 1; + if (loadCall === 3) { + return { + version: 3, + activeIndex: 0, + activeIndexByFamily: { codex: 0 }, + accounts: structuredClone(staleAccounts), + }; + } + return structuredClone(storage); + }); + promptLoginModeMock + .mockResolvedValueOnce({ + mode: "manage", + switchAccountIndex: 28, + selectedAccountNumber: 1, + }) + .mockResolvedValueOnce({ mode: "cancel" }); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js"); + + const exitCode = await runCodexMultiAuthCli(["auth", "login"]); + + expect(exitCode).toBe(0); + expect(errorSpy).toHaveBeenCalledWith( + "Selected account 1 is no longer available.", + ); + expect( + errorSpy.mock.calls.some((call) => String(call[0]).includes("29")), + ).toBe(false); + expect(logSpy).toHaveBeenCalledWith("Cancelled."); + }); + it("marks newly added login account active so smart sort reflects it immediately", async () => { const now = Date.now(); let storageState: {