@@ -452,7 +452,7 @@ describe("create_issue", () => {
452452 const result = await handler ( { title : "Test" } ) ;
453453
454454 expect ( result . success ) . toBe ( false ) ;
455- expect ( result . error ) . toBe ( "API Error" ) ;
455+ expect ( result . error ) . toContain ( "API Error" ) ;
456456 } ) ;
457457 } ) ;
458458
@@ -667,4 +667,95 @@ describe("create_issue", () => {
667667 expect ( mockGithub . rest . issues . create ) . not . toHaveBeenCalled ( ) ;
668668 } ) ;
669669 } ) ;
670+
671+ describe ( "retry on rate limit errors" , ( ) => {
672+ beforeEach ( ( ) => {
673+ vi . useFakeTimers ( ) ;
674+ } ) ;
675+
676+ afterEach ( ( ) => {
677+ vi . useRealTimers ( ) ;
678+ } ) ;
679+
680+ it ( "should retry issue creation on transient rate limit error and succeed" , async ( ) => {
681+ mockGithub . rest . issues . create = vi
682+ . fn ( )
683+ . mockRejectedValueOnce ( new Error ( "Secondary rate limit hit" ) )
684+ . mockResolvedValue ( {
685+ data : {
686+ number : 456 ,
687+ html_url : "https://github.com/owner/repo/issues/456" ,
688+ title : "Retried Issue" ,
689+ } ,
690+ } ) ;
691+
692+ const handler = await main ( { } ) ;
693+ const resultPromise = handler ( {
694+ title : "Retried Issue" ,
695+ body : "Test body" ,
696+ } ) ;
697+
698+ await vi . runAllTimersAsync ( ) ;
699+ const result = await resultPromise ;
700+
701+ expect ( result . success ) . toBe ( true ) ;
702+ expect ( result . number ) . toBe ( 456 ) ;
703+ expect ( mockGithub . rest . issues . create ) . toHaveBeenCalledTimes ( 2 ) ;
704+ } ) ;
705+
706+ it ( "should fail after exhausting retries on persistent rate limit error" , async ( ) => {
707+ mockGithub . rest . issues . create = vi . fn ( ) . mockRejectedValue ( new Error ( "Secondary rate limit hit" ) ) ;
708+
709+ const handler = await main ( { } ) ;
710+ const resultPromise = handler ( {
711+ title : "Failing Issue" ,
712+ body : "Test body" ,
713+ } ) ;
714+
715+ await vi . runAllTimersAsync ( ) ;
716+ const result = await resultPromise ;
717+
718+ expect ( result . success ) . toBe ( false ) ;
719+ expect ( result . error ) . toBeDefined ( ) ;
720+ // 1 initial + 3 retries = 4 calls
721+ expect ( mockGithub . rest . issues . create ) . toHaveBeenCalledTimes ( 4 ) ;
722+ } ) ;
723+
724+ it ( "should have retry delays that never exceed maxDelayMs + jitterMs" , async ( ) => {
725+ const setTimeoutSpy = vi . spyOn ( globalThis , "setTimeout" ) ;
726+
727+ mockGithub . rest . issues . create = vi
728+ . fn ( )
729+ . mockRejectedValueOnce ( new Error ( "Secondary rate limit hit" ) )
730+ . mockRejectedValueOnce ( new Error ( "Secondary rate limit hit" ) )
731+ . mockResolvedValue ( {
732+ data : {
733+ number : 789 ,
734+ html_url : "https://github.com/owner/repo/issues/789" ,
735+ title : "Bounded Delay Issue" ,
736+ } ,
737+ } ) ;
738+
739+ const handler = await main ( { } ) ;
740+ const resultPromise = handler ( {
741+ title : "Bounded Delay Issue" ,
742+ body : "Test body" ,
743+ } ) ;
744+
745+ await vi . runAllTimersAsync ( ) ;
746+ await resultPromise ;
747+
748+ // create_issue uses { initialDelayMs: 15000, maxDelayMs: 45000, jitterMs: 10000 }
749+ // Maximum possible delay per retry = maxDelayMs + jitterMs = 55000ms
750+ const maxBound = 55000 ;
751+ // Filter out short setTimeout calls (e.g. from test infrastructure) to isolate retry delays
752+ const sleepDelays = setTimeoutSpy . mock . calls . filter ( ( [ , ms ] ) => ms > 1000 ) . map ( ( [ , ms ] ) => ms ) ;
753+
754+ for ( const delay of sleepDelays ) {
755+ expect ( delay ) . toBeLessThanOrEqual ( maxBound ) ;
756+ }
757+
758+ setTimeoutSpy . mockRestore ( ) ;
759+ } ) ;
760+ } ) ;
670761} ) ;
0 commit comments