@@ -4260,10 +4260,9 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => {
42604260 expect ( showRuntimeToastMock ) . not . toHaveBeenCalled ( ) ;
42614261 } ) ;
42624262
4263- it ( "applies the default cooldown when a 429 has no parsed retry metadata" , async ( ) => {
4263+ it ( "skips rate-limit marking when a 429 has no parsed retry metadata (entitlement-style) " , async ( ) => {
42644264 const { AccountManager } = await import ( "../lib/accounts.js" ) ;
42654265 const fetchHelpersModule = await import ( "../lib/request/fetch-helpers.js" ) ;
4266- const rateLimitBackoffModule = await import ( "../lib/request/rate-limit-backoff.js" ) ;
42674266
42684267 const markRateLimitedWithReason = vi . fn ( ) ;
42694268 const manager = {
@@ -4318,13 +4317,95 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => {
43184317 rateLimit : undefined ,
43194318 errorBody : "rate limited" ,
43204319 } as never ) ;
4320+ globalThis . fetch = vi
4321+ . fn ( )
4322+ . mockResolvedValueOnce ( new Response ( "rate limited" , { status : 429 } ) ) ;
4323+
4324+ const mockClient = createMockClient ( ) ;
4325+ const { OpenAIOAuthPlugin } = await import ( "../index.js" ) ;
4326+ const plugin = await OpenAIOAuthPlugin ( { client : mockClient } as never ) as unknown as PluginType ;
4327+ const sdk = await plugin . auth . loader ( getOAuthAuth , { options : { } , models : { } } ) ;
4328+ const response = await sdk . fetch ! ( "https://api.openai.com/v1/chat/completions" , {
4329+ method : "POST" ,
4330+ body : JSON . stringify ( { model : "gpt-5.1" } ) ,
4331+ } ) ;
4332+
4333+ expect ( response . status ) . toBe ( 429 ) ;
4334+ expect ( globalThis . fetch ) . toHaveBeenCalledTimes ( 1 ) ;
4335+ expect ( markRateLimitedWithReason ) . not . toHaveBeenCalled ( ) ;
4336+ } ) ;
4337+
4338+ it ( "rotates account when upstream 404 usage_limit_reached is remapped to 429" , async ( ) => {
4339+ const { AccountManager } = await import ( "../lib/accounts.js" ) ;
4340+ const fetchHelpersModule = await import ( "../lib/request/fetch-helpers.js" ) ;
4341+ const rateLimitBackoffModule = await import ( "../lib/request/rate-limit-backoff.js" ) ;
4342+
4343+ const markRateLimitedWithReason = vi . fn ( ) ;
4344+ const manager = {
4345+ getAccountCount : ( ) => 1 ,
4346+ getCurrentOrNextForFamilyHybrid : ( ) => ( {
4347+ index : 0 ,
4348+ accountId : "acc-1" ,
4349+ email : "alpha@example.com" ,
4350+ refreshToken : "refresh-1" ,
4351+ } ) ,
4352+ getCurrentOrNextForFamily : ( ) => ( {
4353+ index : 0 ,
4354+ accountId : "acc-1" ,
4355+ email : "alpha@example.com" ,
4356+ refreshToken : "refresh-1" ,
4357+ } ) ,
4358+ getCurrentWorkspace : ( ) => null ,
4359+ getAccountByIndex : ( ) => null ,
4360+ getAccountsSnapshot : ( ) => [ ] ,
4361+ isAccountAvailableForFamily : ( ) => true ,
4362+ toAuthDetails : ( ) => ( {
4363+ type : "oauth" as const ,
4364+ access : "access-token" ,
4365+ refresh : "refresh-1" ,
4366+ expires : Date . now ( ) + 60_000 ,
4367+ } ) ,
4368+ hasRefreshToken : ( ) => true ,
4369+ saveToDiskDebounced : ( ) => { } ,
4370+ updateFromAuth : ( ) => { } ,
4371+ clearAuthFailures : ( ) => { } ,
4372+ incrementAuthFailures : ( ) => 1 ,
4373+ saveToDisk : async ( ) => { } ,
4374+ markAccountCoolingDown : ( ) => { } ,
4375+ markRateLimited : ( ) => { } ,
4376+ markRateLimitedWithReason,
4377+ consumeToken : ( ) => true ,
4378+ refundToken : ( ) => { } ,
4379+ syncCodexCliActiveSelectionForIndex : async ( ) => { } ,
4380+ markSwitched : ( ) => { } ,
4381+ removeAccount : ( ) => { } ,
4382+ recordFailure : ( ) => { } ,
4383+ recordSuccess : ( ) => { } ,
4384+ recordRateLimit : ( ) => { } ,
4385+ getMinWaitTimeForFamily : ( ) => 0 ,
4386+ shouldShowAccountToast : ( ) => true ,
4387+ markToastShown : ( ) => { } ,
4388+ setActiveIndex : ( ) => null ,
4389+ } ;
4390+ vi . spyOn ( AccountManager , "loadFromDisk" ) . mockResolvedValue ( manager as never ) ;
4391+ vi . mocked ( fetchHelpersModule . handleErrorResponse ) . mockResolvedValueOnce ( {
4392+ response : new Response ( JSON . stringify ( { error : { code : "usage_limit_reached" , message : "Usage limit reached" } } ) , { status : 429 } ) ,
4393+ rateLimit : {
4394+ retryAfterMs : 2 * 60 * 60 * 1000 ,
4395+ code : "usage_limit_reached" ,
4396+ } ,
4397+ errorBody : JSON . stringify ( { error : { code : "usage_limit_reached" , message : "Usage limit reached" } } ) ,
4398+ } as never ) ;
43214399 vi . mocked ( rateLimitBackoffModule . getRateLimitBackoff ) . mockReturnValueOnce ( {
4322- attempt : 1 ,
4323- delayMs : 5_000 ,
4400+ attempt : 2 ,
4401+ delayMs : 1000 ,
43244402 } ) ;
43254403 globalThis . fetch = vi
43264404 . fn ( )
4327- . mockResolvedValueOnce ( new Response ( "rate limited" , { status : 429 } ) ) ;
4405+ . mockResolvedValueOnce (
4406+ new Response ( JSON . stringify ( { error : { code : "usage_limit_reached" , message : "Usage limit reached" } } ) , { status : 404 } ) ,
4407+ )
4408+ . mockResolvedValueOnce ( new Response ( JSON . stringify ( { content : "ok" } ) , { status : 200 } ) ) ;
43284409
43294410 const mockClient = createMockClient ( ) ;
43304411 const { OpenAIOAuthPlugin } = await import ( "../index.js" ) ;
@@ -4336,11 +4417,12 @@ describe("OpenAIOAuthPlugin runtime toast forwarding", () => {
43364417 } ) ;
43374418
43384419 expect ( response . status ) . toBe ( 503 ) ;
4420+ expect ( globalThis . fetch ) . toHaveBeenCalledTimes ( 1 ) ;
43394421 expect ( markRateLimitedWithReason ) . toHaveBeenCalledWith (
43404422 expect . objectContaining ( { index : 0 } ) ,
4341- 60_000 ,
4423+ 2 * 60 * 60 * 1000 ,
43424424 "gpt-5.1" ,
4343- "unknown" ,
4425+ expect . any ( String ) ,
43444426 "gpt-5.1" ,
43454427 ) ;
43464428 } ) ;
0 commit comments