@@ -2039,6 +2039,121 @@ describe("OpenAIOAuthPlugin persistAccountPool", () => {
20392039 ) . toEqual ( [ "refresh-a" , "refresh-b" , "refresh-c" ] ) ;
20402040 } ) ;
20412041
2042+ it ( "serializes concurrent manual logins through the storage transaction queue" , async ( ) => {
2043+ const createDeferred = ( ) => {
2044+ let resolve ! : ( ) => void ;
2045+ const promise = new Promise < void > ( ( res ) => {
2046+ resolve = res ;
2047+ } ) ;
2048+ return { promise, resolve } ;
2049+ } ;
2050+
2051+ mockStorage . accounts = [ ] ;
2052+ mockStorage . activeIndex = 0 ;
2053+ mockStorage . activeIndexByFamily = { } ;
2054+
2055+ const firstPersist = createDeferred ( ) ;
2056+ saveAccountsMock
2057+ . mockImplementationOnce ( async ( storage ) => {
2058+ await firstPersist . promise ;
2059+ mockStorage . version = storage . version ;
2060+ mockStorage . accounts = storage . accounts . map ( ( account ) => ( { ...account } ) ) ;
2061+ mockStorage . activeIndex = storage . activeIndex ;
2062+ mockStorage . activeIndexByFamily = {
2063+ ...( storage . activeIndexByFamily ?? { } ) ,
2064+ } ;
2065+ } )
2066+ . mockImplementation ( async ( storage ) => {
2067+ mockStorage . version = storage . version ;
2068+ mockStorage . accounts = storage . accounts . map ( ( account ) => ( { ...account } ) ) ;
2069+ mockStorage . activeIndex = storage . activeIndex ;
2070+ mockStorage . activeIndexByFamily = {
2071+ ...( storage . activeIndexByFamily ?? { } ) ,
2072+ } ;
2073+ } ) ;
2074+
2075+ const authModule = await import ( "../lib/auth/auth.js" ) ;
2076+ const accountsModule = await import ( "../lib/accounts.js" ) ;
2077+ vi . mocked ( authModule . createAuthorizationFlow )
2078+ . mockResolvedValueOnce ( {
2079+ pkce : { verifier : "persist-concurrent-verifier-1" , challenge : "persist-concurrent-challenge-1" } ,
2080+ state : "persist-concurrent-state-1" ,
2081+ url : "https://auth.openai.com/test?state=persist-concurrent-state-1" ,
2082+ } )
2083+ . mockResolvedValueOnce ( {
2084+ pkce : { verifier : "persist-concurrent-verifier-2" , challenge : "persist-concurrent-challenge-2" } ,
2085+ state : "persist-concurrent-state-2" ,
2086+ url : "https://auth.openai.com/test?state=persist-concurrent-state-2" ,
2087+ } ) ;
2088+ vi . mocked ( authModule . exchangeAuthorizationCode )
2089+ . mockResolvedValueOnce ( {
2090+ type : "success" ,
2091+ access : "access-token-1" ,
2092+ refresh : "refresh-1" ,
2093+ expires : Date . now ( ) + 3600_000 ,
2094+ idToken : undefined ,
2095+ } )
2096+ . mockResolvedValueOnce ( {
2097+ type : "success" ,
2098+ access : "access-token-2" ,
2099+ refresh : "refresh-2" ,
2100+ expires : Date . now ( ) + 3600_000 ,
2101+ idToken : undefined ,
2102+ } ) ;
2103+ vi . mocked ( accountsModule . extractAccountEmail )
2104+ . mockReturnValueOnce ( "alpha@example.com" )
2105+ . mockReturnValueOnce ( "beta@example.com" ) ;
2106+ vi . mocked ( accountsModule . extractAccountId )
2107+ . mockReturnValueOnce ( "workspace-alpha" )
2108+ . mockReturnValueOnce ( "workspace-beta" ) ;
2109+
2110+ const mockClient = createMockClient ( ) ;
2111+ const { OpenAIOAuthPlugin } = await import ( "../index.js" ) ;
2112+ const plugin =
2113+ ( await OpenAIOAuthPlugin ( {
2114+ client : mockClient ,
2115+ } as never ) ) as unknown as PluginType ;
2116+ const manualMethod = plugin . auth . methods [ 1 ] as unknown as {
2117+ authorize : ( ) => Promise < {
2118+ callback : ( input : string ) => Promise < { type : string } > ;
2119+ } > ;
2120+ } ;
2121+
2122+ const flowOne = await manualMethod . authorize ( ) ;
2123+ const flowTwo = await manualMethod . authorize ( ) ;
2124+ const firstCallback = flowOne . callback (
2125+ "http://127.0.0.1:1455/auth/callback?code=alpha&state=persist-concurrent-state-1" ,
2126+ ) ;
2127+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
2128+ const secondCallback = flowTwo . callback (
2129+ "http://127.0.0.1:1455/auth/callback?code=beta&state=persist-concurrent-state-2" ,
2130+ ) ;
2131+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
2132+
2133+ expect ( saveAccountsMock ) . toHaveBeenCalledTimes ( 1 ) ;
2134+
2135+ firstPersist . resolve ( ) ;
2136+
2137+ const [ firstResult , secondResult ] = await Promise . all ( [
2138+ firstCallback ,
2139+ secondCallback ,
2140+ ] ) ;
2141+
2142+ expect ( firstResult . type ) . toBe ( "success" ) ;
2143+ expect ( secondResult . type ) . toBe ( "success" ) ;
2144+ expect ( saveAccountsMock ) . toHaveBeenCalledTimes ( 2 ) ;
2145+ expect ( mockStorage . accounts ) . toHaveLength ( 2 ) ;
2146+ expect (
2147+ mockStorage . accounts . map ( ( account ) => ( {
2148+ email : account . email ,
2149+ refreshToken : account . refreshToken ,
2150+ } ) ) ,
2151+ ) . toEqual ( [
2152+ { email : "alpha@example.com" , refreshToken : "refresh-1" } ,
2153+ { email : "beta@example.com" , refreshToken : "refresh-2" } ,
2154+ ] ) ;
2155+ } ) ;
2156+
20422157 it ( "updates a unique shared accountId entry when a login has no email claim" , async ( ) => {
20432158 process . env . CODEX_AUTH_ACCOUNT_ID = "shared-workspace" ;
20442159 mockStorage . accounts = [
0 commit comments