@@ -208,6 +208,12 @@ vi.mock("../lib/request/rate-limit-backoff.js", () => ({
208208 refreshAndUpdateToken : vi . fn ( async ( auth : unknown ) => auth ) ,
209209 createCodexHeaders : vi . fn ( ( ) => new Headers ( ) ) ,
210210 handleErrorResponse : vi . fn ( async ( response : Response ) => ( { response } ) ) ,
211+ isWorkspaceDisabledError : ( status : number , code : string , bodyText : string ) =>
212+ status === 403 &&
213+ ( code . toLowerCase ( ) . includes ( "workspace_disabled" ) ||
214+ code . toLowerCase ( ) . includes ( "workspace_expired" ) ||
215+ bodyText . toLowerCase ( ) . includes ( "workspace disabled" ) ||
216+ bodyText . toLowerCase ( ) . includes ( "workspace expired" ) ) ,
211217 getUnsupportedCodexModelInfo : vi . fn ( ( ) => ( { isUnsupported : false } ) ) ,
212218 resolveUnsupportedCodexFallbackModel : vi . fn ( ( ) => undefined ) ,
213219 shouldFallbackToGpt52OnUnsupportedGpt53 : vi . fn ( ( ) => false ) ,
@@ -240,6 +246,11 @@ const mockStorage = {
240246 activeIndexByFamily : { } as Record < string , number > ,
241247} ;
242248
249+ const cloneMockAccount = ( account : ( typeof mockStorage . accounts ) [ number ] ) => ( {
250+ ...account ,
251+ workspaces : account . workspaces ?. map ( ( workspace ) => ( { ...workspace } ) ) ,
252+ } ) ;
253+
243254const loadAccountsMock = vi . fn ( async ( ) => mockStorage ) ;
244255const saveAccountsMock = vi . fn (
245256 async ( storage : {
@@ -249,7 +260,7 @@ const saveAccountsMock = vi.fn(
249260 activeIndexByFamily ?: Record < string , number > ;
250261 } ) => {
251262 mockStorage . version = storage . version ;
252- mockStorage . accounts = storage . accounts . map ( ( account ) => ( { ... account } ) ) ;
263+ mockStorage . accounts = storage . accounts . map ( ( account ) => cloneMockAccount ( account ) ) ;
253264 mockStorage . activeIndex = storage . activeIndex ;
254265 mockStorage . activeIndexByFamily = {
255266 ...( storage . activeIndexByFamily ?? { } ) ,
@@ -283,7 +294,7 @@ const withAccountStorageTransactionMock = vi.fn(
283294 handler (
284295 {
285296 version : 3 ,
286- accounts : mockStorage . accounts . map ( ( account ) => ( { ... account } ) ) ,
297+ accounts : mockStorage . accounts . map ( ( account ) => cloneMockAccount ( account ) ) ,
287298 activeIndex : mockStorage . activeIndex ,
288299 activeIndexByFamily : { ...mockStorage . activeIndexByFamily } ,
289300 } ,
@@ -331,6 +342,10 @@ vi.mock("../lib/storage.js", async () => {
331342
332343const extractAccountEmailMock = vi . fn ( ( ) => "user@example.com" ) ;
333344const extractAccountIdMock = vi . fn ( ( ) => "account-1" ) ;
345+ const getAccountIdCandidatesMock = vi . fn ( ( ) => [ { accountId : "acc-1" , source : "token" , label : "Test" } ] ) ;
346+ const selectBestAccountCandidateMock = vi . fn (
347+ ( candidates : Array < { accountId : string } > ) => candidates [ 0 ] ?? null ,
348+ ) ;
334349
335350vi . mock ( "../lib/accounts.js" , ( ) => {
336351 class MockAccountManager {
@@ -429,8 +444,8 @@ vi.mock("../lib/accounts.js", () => {
429444
430445 return {
431446 AccountManager : MockAccountManager ,
432- getAccountIdCandidates : ( ) => [ { accountId : "acc-1" , source : "token" , label : "Test" } ] ,
433- selectBestAccountCandidate : ( candidates : Array < { accountId : string } > ) => candidates [ 0 ] ?? null ,
447+ getAccountIdCandidates : getAccountIdCandidatesMock ,
448+ selectBestAccountCandidate : selectBestAccountCandidateMock ,
434449 extractAccountEmail : extractAccountEmailMock ,
435450 extractAccountId : extractAccountIdMock ,
436451 resolveRequestAccountId : ( _storedId : string | undefined , _source : string | undefined , tokenId : string | undefined ) => tokenId ,
@@ -513,6 +528,14 @@ describe("OpenAIOAuthPlugin", () => {
513528 extractAccountEmailMock . mockImplementation ( ( ) => "user@example.com" ) ;
514529 extractAccountIdMock . mockReset ( ) ;
515530 extractAccountIdMock . mockImplementation ( ( ) => "account-1" ) ;
531+ getAccountIdCandidatesMock . mockReset ( ) ;
532+ getAccountIdCandidatesMock . mockImplementation ( ( ) => [
533+ { accountId : "acc-1" , source : "token" , label : "Test" } ,
534+ ] ) ;
535+ selectBestAccountCandidateMock . mockReset ( ) ;
536+ selectBestAccountCandidateMock . mockImplementation (
537+ ( candidates : Array < { accountId : string } > ) => candidates [ 0 ] ?? null ,
538+ ) ;
516539
517540 mockStorage . accounts = [ ] ;
518541 mockStorage . activeIndex = 0 ;
@@ -1924,6 +1947,14 @@ describe("OpenAIOAuthPlugin resolveAccountSelection", () => {
19241947describe ( "OpenAIOAuthPlugin persistAccountPool" , ( ) => {
19251948 beforeEach ( ( ) => {
19261949 vi . clearAllMocks ( ) ;
1950+ getAccountIdCandidatesMock . mockReset ( ) ;
1951+ getAccountIdCandidatesMock . mockImplementation ( ( ) => [
1952+ { accountId : "acc-1" , source : "token" , label : "Test" } ,
1953+ ] ) ;
1954+ selectBestAccountCandidateMock . mockReset ( ) ;
1955+ selectBestAccountCandidateMock . mockImplementation (
1956+ ( candidates : Array < { accountId : string } > ) => candidates [ 0 ] ?? null ,
1957+ ) ;
19271958 mockStorage . accounts = [ ] ;
19281959 mockStorage . activeIndex = 0 ;
19291960 mockStorage . activeIndexByFamily = { } ;
@@ -1968,6 +1999,62 @@ describe("OpenAIOAuthPlugin persistAccountPool", () => {
19681999 expect ( mockStorage . accounts ) . toHaveLength ( 1 ) ;
19692000 } ) ;
19702001
2002+ it ( "initializes currentWorkspaceIndex for a newly persisted selected workspace" , async ( ) => {
2003+ const authModule = await import ( "../lib/auth/auth.js" ) ;
2004+ const accountsModule = await import ( "../lib/accounts.js" ) ;
2005+ vi . mocked ( authModule . createAuthorizationFlow ) . mockResolvedValueOnce ( {
2006+ pkce : { verifier : "persist-new-workspace-index" , challenge : "persist-new-workspace-index" } ,
2007+ state : "persist-new-workspace-index" ,
2008+ url : "https://auth.openai.com/test?state=persist-new-workspace-index" ,
2009+ } ) ;
2010+ vi . mocked ( authModule . exchangeAuthorizationCode ) . mockResolvedValueOnce ( {
2011+ type : "success" ,
2012+ access : "access-token" ,
2013+ refresh : "refresh-token" ,
2014+ expires : Date . now ( ) + 3600_000 ,
2015+ idToken : "id-token" ,
2016+ } ) ;
2017+ vi . mocked ( accountsModule . getAccountIdCandidates ) . mockReturnValueOnce ( [
2018+ { accountId : "workspace-a" , source : "org" , label : "Workspace A" } ,
2019+ { accountId : "workspace-b" , source : "org" , label : "Workspace B" , isDefault : true } ,
2020+ { accountId : "workspace-c" , source : "org" , label : "Workspace C" } ,
2021+ ] ) ;
2022+ vi . mocked ( accountsModule . selectBestAccountCandidate ) . mockImplementationOnce (
2023+ ( candidates ) => candidates [ 1 ] ?? null ,
2024+ ) ;
2025+
2026+ const mockClient = createMockClient ( ) ;
2027+ const { OpenAIOAuthPlugin } = await import ( "../index.js" ) ;
2028+ const plugin =
2029+ ( await OpenAIOAuthPlugin ( {
2030+ client : mockClient ,
2031+ } as never ) ) as unknown as PluginType ;
2032+ const manualMethod = plugin . auth . methods [ 1 ] as unknown as {
2033+ authorize : ( ) => Promise < {
2034+ callback : ( input : string ) => Promise < { type : string } > ;
2035+ } > ;
2036+ } ;
2037+
2038+ const flow = await manualMethod . authorize ( ) ;
2039+ const result = await flow . callback (
2040+ "http://127.0.0.1:1455/auth/callback?code=abc123&state=persist-new-workspace-index" ,
2041+ ) ;
2042+
2043+ expect ( result . type ) . toBe ( "success" ) ;
2044+ expect ( mockStorage . accounts ) . toHaveLength ( 1 ) ;
2045+ expect ( mockStorage . accounts [ 0 ] ) . toEqual (
2046+ expect . objectContaining ( {
2047+ accountId : "workspace-b" ,
2048+ currentWorkspaceIndex : 1 ,
2049+ workspaces : [
2050+ { id : "workspace-a" , name : "Workspace A" , enabled : true , isDefault : undefined } ,
2051+ { id : "workspace-b" , name : "Workspace B" , enabled : true , isDefault : true } ,
2052+ { id : "workspace-c" , name : "Workspace C" , enabled : true , isDefault : undefined } ,
2053+ ] ,
2054+ } ) ,
2055+ ) ;
2056+ } ) ;
2057+
19712058 it ( "preserves distinct accountId plus email pairs during manual login" , async ( ) => {
19722059 process . env . CODEX_AUTH_ACCOUNT_ID = "shared-workspace" ;
19732060 mockStorage . accounts = [
@@ -2459,15 +2546,15 @@ describe("OpenAIOAuthPlugin persistAccountPool", () => {
24592546 . mockImplementationOnce ( async ( storage ) => {
24602547 await firstPersist . promise ;
24612548 mockStorage . version = storage . version ;
2462- mockStorage . accounts = storage . accounts . map ( ( account ) => ( { ... account } ) ) ;
2549+ mockStorage . accounts = storage . accounts . map ( ( account ) => cloneMockAccount ( account ) ) ;
24632550 mockStorage . activeIndex = storage . activeIndex ;
24642551 mockStorage . activeIndexByFamily = {
24652552 ...( storage . activeIndexByFamily ?? { } ) ,
24662553 } ;
24672554 } )
24682555 . mockImplementation ( async ( storage ) => {
24692556 mockStorage . version = storage . version ;
2470- mockStorage . accounts = storage . accounts . map ( ( account ) => ( { ... account } ) ) ;
2557+ mockStorage . accounts = storage . accounts . map ( ( account ) => cloneMockAccount ( account ) ) ;
24712558 mockStorage . activeIndex = storage . activeIndex ;
24722559 mockStorage . activeIndexByFamily = {
24732560 ...( storage . activeIndexByFamily ?? { } ) ,
0 commit comments