@@ -772,10 +772,8 @@ describe("Cline", () => {
772772 false ,
773773 )
774774
775- // Calculate expected delay calls based on exponential backoff
776- const exponentialDelay = Math . ceil ( baseDelay * Math . pow ( 2 , 1 ) ) // retryAttempt = 1
777- const rateLimitDelay = baseDelay // Initial rate limit delay
778- const totalExpectedDelays = exponentialDelay + rateLimitDelay
775+ // Calculate expected delay calls for countdown
776+ const totalExpectedDelays = baseDelay // One delay per second for countdown
779777 expect ( mockDelay ) . toHaveBeenCalledTimes ( totalExpectedDelays )
780778 expect ( mockDelay ) . toHaveBeenCalledWith ( 1000 )
781779
@@ -786,6 +784,122 @@ describe("Cline", () => {
786784 )
787785 } )
788786
787+ it ( "should not apply retry delay twice" , async ( ) => {
788+ const cline = new Cline ( mockProvider , mockApiConfig , undefined , false , false , undefined , "test task" )
789+
790+ // Mock delay to track countdown timing
791+ const mockDelay = jest . fn ( ) . mockResolvedValue ( undefined )
792+ jest . spyOn ( require ( "delay" ) , "default" ) . mockImplementation ( mockDelay )
793+
794+ // Mock say to track messages
795+ const saySpy = jest . spyOn ( cline , "say" )
796+
797+ // Create a stream that fails on first chunk
798+ const mockError = new Error ( "API Error" )
799+ const mockFailedStream = {
800+ async * [ Symbol . asyncIterator ] ( ) {
801+ throw mockError
802+ } ,
803+ async next ( ) {
804+ throw mockError
805+ } ,
806+ async return ( ) {
807+ return { done : true , value : undefined }
808+ } ,
809+ async throw ( e : any ) {
810+ throw e
811+ } ,
812+ async [ Symbol . asyncDispose ] ( ) {
813+ // Cleanup
814+ } ,
815+ } as AsyncGenerator < ApiStreamChunk >
816+
817+ // Create a successful stream for retry
818+ const mockSuccessStream = {
819+ async * [ Symbol . asyncIterator ] ( ) {
820+ yield { type : "text" , text : "Success" }
821+ } ,
822+ async next ( ) {
823+ return { done : true , value : { type : "text" , text : "Success" } }
824+ } ,
825+ async return ( ) {
826+ return { done : true , value : undefined }
827+ } ,
828+ async throw ( e : any ) {
829+ throw e
830+ } ,
831+ async [ Symbol . asyncDispose ] ( ) {
832+ // Cleanup
833+ } ,
834+ } as AsyncGenerator < ApiStreamChunk >
835+
836+ // Mock createMessage to fail first then succeed
837+ let firstAttempt = true
838+ jest . spyOn ( cline . api , "createMessage" ) . mockImplementation ( ( ) => {
839+ if ( firstAttempt ) {
840+ firstAttempt = false
841+ return mockFailedStream
842+ }
843+ return mockSuccessStream
844+ } )
845+
846+ // Set alwaysApproveResubmit and requestDelaySeconds
847+ mockProvider . getState = jest . fn ( ) . mockResolvedValue ( {
848+ alwaysApproveResubmit : true ,
849+ requestDelaySeconds : 3 ,
850+ } )
851+
852+ // Mock previous API request message
853+ cline . clineMessages = [
854+ {
855+ ts : Date . now ( ) ,
856+ type : "say" ,
857+ say : "api_req_started" ,
858+ text : JSON . stringify ( {
859+ tokensIn : 100 ,
860+ tokensOut : 50 ,
861+ cacheWrites : 0 ,
862+ cacheReads : 0 ,
863+ request : "test request" ,
864+ } ) ,
865+ } ,
866+ ]
867+
868+ // Trigger API request
869+ const iterator = cline . attemptApiRequest ( 0 )
870+ await iterator . next ( )
871+
872+ // Verify delay is only applied for the countdown
873+ const baseDelay = 3 // from requestDelaySeconds
874+ const expectedDelayCount = baseDelay // One delay per second for countdown
875+ expect ( mockDelay ) . toHaveBeenCalledTimes ( expectedDelayCount )
876+ expect ( mockDelay ) . toHaveBeenCalledWith ( 1000 ) // Each delay should be 1 second
877+
878+ // Verify countdown messages were only shown once
879+ const retryMessages = saySpy . mock . calls . filter (
880+ ( call ) => call [ 0 ] === "api_req_retry_delayed" && call [ 1 ] ?. includes ( "Retrying in" ) ,
881+ )
882+ expect ( retryMessages ) . toHaveLength ( baseDelay )
883+
884+ // Verify the retry message sequence
885+ for ( let i = baseDelay ; i > 0 ; i -- ) {
886+ expect ( saySpy ) . toHaveBeenCalledWith (
887+ "api_req_retry_delayed" ,
888+ expect . stringContaining ( `Retrying in ${ i } seconds` ) ,
889+ undefined ,
890+ true ,
891+ )
892+ }
893+
894+ // Verify final retry message
895+ expect ( saySpy ) . toHaveBeenCalledWith (
896+ "api_req_retry_delayed" ,
897+ expect . stringContaining ( "Retrying now" ) ,
898+ undefined ,
899+ false ,
900+ )
901+ } )
902+
789903 describe ( "loadContext" , ( ) => {
790904 it ( "should process mentions in task and feedback tags" , async ( ) => {
791905 const cline = new Cline (
0 commit comments