@@ -9,6 +9,11 @@ import { GeminiHandler } from "../gemini"
99
1010const GEMINI_20_FLASH_THINKING_NAME = "gemini-2.0-flash-thinking-exp-1219"
1111
12+ // Mock delay module
13+ vitest . mock ( "delay" , ( ) => ( {
14+ default : vitest . fn ( ( ) => Promise . resolve ( ) ) ,
15+ } ) )
16+
1217describe ( "GeminiHandler" , ( ) => {
1318 let handler : GeminiHandler
1419
@@ -102,6 +107,107 @@ describe("GeminiHandler", () => {
102107 }
103108 } ) . rejects . toThrow ( )
104109 } )
110+
111+ it ( "should retry on rate limit errors" , async ( ) => {
112+ const mockError = new Error ( "Rate limit exceeded" )
113+ // @ts -ignore - adding status property to error
114+ mockError . status = 429
115+
116+ const mockStream = {
117+ [ Symbol . asyncIterator ] : async function * ( ) {
118+ yield {
119+ candidates : [
120+ {
121+ content : {
122+ parts : [ { text : "Success after retry" } ] ,
123+ } ,
124+ } ,
125+ ] ,
126+ }
127+ } ,
128+ }
129+
130+ const generateContentStreamMock = handler [ "client" ] . models . generateContentStream as any
131+ generateContentStreamMock . mockRejectedValueOnce ( mockError ) . mockResolvedValueOnce ( mockStream )
132+
133+ const chunks : any [ ] = [ ]
134+ for await ( const chunk of handler . createMessage ( systemPrompt , mockMessages ) ) {
135+ chunks . push ( chunk )
136+ }
137+
138+ expect ( generateContentStreamMock ) . toHaveBeenCalledTimes ( 2 )
139+ expect ( chunks [ 0 ] ) . toEqual ( { type : "text" , text : "Success after retry" } )
140+ } )
141+
142+ it ( "should handle blank responses" , async ( ) => {
143+ const mockStream = {
144+ [ Symbol . asyncIterator ] : async function * ( ) {
145+ // Yield empty chunks
146+ yield { }
147+ yield { candidates : [ ] }
148+ } ,
149+ }
150+
151+ ; ( handler [ "client" ] . models . generateContentStream as any ) . mockResolvedValue ( mockStream )
152+
153+ const stream = handler . createMessage ( systemPrompt , mockMessages )
154+
155+ await expect ( async ( ) => {
156+ for await ( const _chunk of stream ) {
157+ // Should throw due to blank response
158+ }
159+ } ) . rejects . toThrow (
160+ t ( "common:errors.gemini.generate_stream" , { error : "Received blank response from Gemini API" } ) ,
161+ )
162+ } )
163+
164+ it ( "should retry on server errors" , async ( ) => {
165+ const mockError = new Error ( "Internal server error" )
166+ // @ts -ignore - adding status property to error
167+ mockError . status = 500
168+
169+ const mockStream = {
170+ [ Symbol . asyncIterator ] : async function * ( ) {
171+ yield {
172+ candidates : [
173+ {
174+ content : {
175+ parts : [ { text : "Success after server error" } ] ,
176+ } ,
177+ } ,
178+ ] ,
179+ }
180+ } ,
181+ }
182+
183+ const generateContentStreamMock = handler [ "client" ] . models . generateContentStream as any
184+ generateContentStreamMock . mockRejectedValueOnce ( mockError ) . mockResolvedValueOnce ( mockStream )
185+
186+ const chunks : any [ ] = [ ]
187+ for await ( const chunk of handler . createMessage ( systemPrompt , mockMessages ) ) {
188+ chunks . push ( chunk )
189+ }
190+
191+ expect ( generateContentStreamMock ) . toHaveBeenCalledTimes ( 2 )
192+ expect ( chunks [ 0 ] ) . toEqual ( { type : "text" , text : "Success after server error" } )
193+ } )
194+
195+ it ( "should not retry on authentication errors" , async ( ) => {
196+ const mockError = new Error ( "Invalid API key" )
197+ // @ts -ignore - adding status property to error
198+ mockError . status = 401
199+ ; ( handler [ "client" ] . models . generateContentStream as any ) . mockRejectedValue ( mockError )
200+
201+ const stream = handler . createMessage ( systemPrompt , mockMessages )
202+
203+ await expect ( async ( ) => {
204+ for await ( const _chunk of stream ) {
205+ // Should throw without retrying
206+ }
207+ } ) . rejects . toThrow ( )
208+
209+ expect ( handler [ "client" ] . models . generateContentStream ) . toHaveBeenCalledTimes ( 1 )
210+ } )
105211 } )
106212
107213 describe ( "completePrompt" , ( ) => {
@@ -134,14 +240,77 @@ describe("GeminiHandler", () => {
134240 )
135241 } )
136242
137- it ( "should handle empty response" , async ( ) => {
243+ it ( "should handle empty response with error " , async ( ) => {
138244 // Mock the response with empty text
139245 ; ( handler [ "client" ] . models . generateContent as any ) . mockResolvedValue ( {
140246 text : "" ,
141247 } )
142248
249+ await expect ( handler . completePrompt ( "Test prompt" ) ) . rejects . toThrow (
250+ t ( "common:errors.gemini.generate_complete_prompt" , {
251+ error : "Received blank response from Gemini API" ,
252+ } ) ,
253+ )
254+ } )
255+
256+ it ( "should retry on rate limit and succeed" , async ( ) => {
257+ const mockError = new Error ( "Rate limit exceeded" )
258+ // @ts -ignore - adding status property to error
259+ mockError . status = 429
260+
261+ const mockResponse = {
262+ text : "Success after rate limit" ,
263+ candidates : [ ] ,
264+ }
265+
266+ const generateContentMock = handler [ "client" ] . models . generateContent as any
267+ generateContentMock . mockRejectedValueOnce ( mockError ) . mockResolvedValueOnce ( mockResponse )
268+
143269 const result = await handler . completePrompt ( "Test prompt" )
144- expect ( result ) . toBe ( "" )
270+
271+ expect ( generateContentMock ) . toHaveBeenCalledTimes ( 2 )
272+ expect ( result ) . toBe ( "Success after rate limit" )
273+ } )
274+
275+ it ( "should handle network errors with retry" , async ( ) => {
276+ const mockError = new Error ( "Network timeout" )
277+
278+ const mockResponse = {
279+ text : "Success after network error" ,
280+ candidates : [ ] ,
281+ }
282+
283+ const generateContentMock = handler [ "client" ] . models . generateContent as any
284+ generateContentMock . mockRejectedValueOnce ( mockError ) . mockResolvedValueOnce ( mockResponse )
285+
286+ const result = await handler . completePrompt ( "Test prompt" )
287+
288+ expect ( generateContentMock ) . toHaveBeenCalledTimes ( 2 )
289+ expect ( result ) . toBe ( "Success after network error" )
290+ } )
291+
292+ it ( "should respect retry delay from error details" , async ( ) => {
293+ const mockError : any = new Error ( "Rate limit exceeded" )
294+ mockError . status = 429
295+ mockError . errorDetails = [
296+ {
297+ "@type" : "type.googleapis.com/google.rpc.RetryInfo" ,
298+ retryDelay : "5s" ,
299+ } ,
300+ ]
301+
302+ const mockResponse = {
303+ text : "Success with custom retry delay" ,
304+ candidates : [ ] ,
305+ }
306+
307+ const generateContentMock = handler [ "client" ] . models . generateContent as any
308+ generateContentMock . mockRejectedValueOnce ( mockError ) . mockResolvedValueOnce ( mockResponse )
309+
310+ const result = await handler . completePrompt ( "Test prompt" )
311+
312+ expect ( generateContentMock ) . toHaveBeenCalledTimes ( 2 )
313+ expect ( result ) . toBe ( "Success with custom retry delay" )
145314 } )
146315 } )
147316
0 commit comments