@@ -315,6 +315,7 @@ describe('Gemini Client (client.ts)', () => {
315315 getSkipNextSpeakerCheck : vi . fn ( ) . mockReturnValue ( false ) ,
316316 getUseSmartEdit : vi . fn ( ) . mockReturnValue ( false ) ,
317317 getUseModelRouter : vi . fn ( ) . mockReturnValue ( false ) ,
318+ getContinueOnFailedApiCall : vi . fn ( ) ,
318319 getProjectRoot : vi . fn ( ) . mockReturnValue ( '/test/project/root' ) ,
319320 storage : {
320321 getProjectTempDir : vi . fn ( ) . mockReturnValue ( '/test/temp' ) ,
@@ -1288,6 +1289,9 @@ ${JSON.stringify(
12881289 } ) ;
12891290
12901291 it ( 'should stop infinite loop after MAX_TURNS when nextSpeaker always returns model' , async ( ) => {
1292+ vi . spyOn ( client [ 'config' ] , 'getContinueOnFailedApiCall' ) . mockReturnValue (
1293+ true ,
1294+ ) ;
12911295 // Get the mocked checkNextSpeaker function and configure it to trigger infinite loop
12921296 const { checkNextSpeaker } = await import (
12931297 '../utils/nextSpeakerChecker.js'
@@ -1677,6 +1681,131 @@ ${JSON.stringify(
16771681 } ) ;
16781682 } ) ;
16791683
1684+ it ( 'should recursively call sendMessageStream with "Please continue." when InvalidStream event is received' , async ( ) => {
1685+ vi . spyOn ( client [ 'config' ] , 'getContinueOnFailedApiCall' ) . mockReturnValue (
1686+ true ,
1687+ ) ;
1688+ // Arrange
1689+ const mockStream1 = ( async function * ( ) {
1690+ yield { type : GeminiEventType . InvalidStream } ;
1691+ } ) ( ) ;
1692+ const mockStream2 = ( async function * ( ) {
1693+ yield { type : GeminiEventType . Content , value : 'Continued content' } ;
1694+ } ) ( ) ;
1695+
1696+ mockTurnRunFn
1697+ . mockReturnValueOnce ( mockStream1 )
1698+ . mockReturnValueOnce ( mockStream2 ) ;
1699+
1700+ const mockChat : Partial < GeminiChat > = {
1701+ addHistory : vi . fn ( ) ,
1702+ getHistory : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1703+ } ;
1704+ client [ 'chat' ] = mockChat as GeminiChat ;
1705+
1706+ const initialRequest = [ { text : 'Hi' } ] ;
1707+ const promptId = 'prompt-id-invalid-stream' ;
1708+ const signal = new AbortController ( ) . signal ;
1709+
1710+ // Act
1711+ const stream = client . sendMessageStream ( initialRequest , signal , promptId ) ;
1712+ const events = await fromAsync ( stream ) ;
1713+
1714+ // Assert
1715+ expect ( events ) . toEqual ( [
1716+ { type : GeminiEventType . InvalidStream } ,
1717+ { type : GeminiEventType . Content , value : 'Continued content' } ,
1718+ ] ) ;
1719+
1720+ // Verify that turn.run was called twice
1721+ expect ( mockTurnRunFn ) . toHaveBeenCalledTimes ( 2 ) ;
1722+
1723+ // First call with original request
1724+ expect ( mockTurnRunFn ) . toHaveBeenNthCalledWith (
1725+ 1 ,
1726+ expect . any ( String ) ,
1727+ initialRequest ,
1728+ expect . any ( Object ) ,
1729+ ) ;
1730+
1731+ // Second call with "Please continue."
1732+ expect ( mockTurnRunFn ) . toHaveBeenNthCalledWith (
1733+ 2 ,
1734+ expect . any ( String ) ,
1735+ [ { text : 'System: Please continue.' } ] ,
1736+ expect . any ( Object ) ,
1737+ ) ;
1738+ } ) ;
1739+
1740+ it ( 'should not recursively call sendMessageStream with "Please continue." when InvalidStream event is received and flag is false' , async ( ) => {
1741+ vi . spyOn ( client [ 'config' ] , 'getContinueOnFailedApiCall' ) . mockReturnValue (
1742+ false ,
1743+ ) ;
1744+ // Arrange
1745+ const mockStream1 = ( async function * ( ) {
1746+ yield { type : GeminiEventType . InvalidStream } ;
1747+ } ) ( ) ;
1748+
1749+ mockTurnRunFn . mockReturnValueOnce ( mockStream1 ) ;
1750+
1751+ const mockChat : Partial < GeminiChat > = {
1752+ addHistory : vi . fn ( ) ,
1753+ getHistory : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1754+ } ;
1755+ client [ 'chat' ] = mockChat as GeminiChat ;
1756+
1757+ const initialRequest = [ { text : 'Hi' } ] ;
1758+ const promptId = 'prompt-id-invalid-stream' ;
1759+ const signal = new AbortController ( ) . signal ;
1760+
1761+ // Act
1762+ const stream = client . sendMessageStream ( initialRequest , signal , promptId ) ;
1763+ const events = await fromAsync ( stream ) ;
1764+
1765+ // Assert
1766+ expect ( events ) . toEqual ( [ { type : GeminiEventType . InvalidStream } ] ) ;
1767+
1768+ // Verify that turn.run was called only once
1769+ expect ( mockTurnRunFn ) . toHaveBeenCalledTimes ( 1 ) ;
1770+ } ) ;
1771+
1772+ it ( 'should stop recursing after one retry when InvalidStream events are repeatedly received' , async ( ) => {
1773+ vi . spyOn ( client [ 'config' ] , 'getContinueOnFailedApiCall' ) . mockReturnValue (
1774+ true ,
1775+ ) ;
1776+ // Arrange
1777+ // Always return a new invalid stream
1778+ mockTurnRunFn . mockImplementation ( ( ) =>
1779+ ( async function * ( ) {
1780+ yield { type : GeminiEventType . InvalidStream } ;
1781+ } ) ( ) ,
1782+ ) ;
1783+
1784+ const mockChat : Partial < GeminiChat > = {
1785+ addHistory : vi . fn ( ) ,
1786+ getHistory : vi . fn ( ) . mockReturnValue ( [ ] ) ,
1787+ } ;
1788+ client [ 'chat' ] = mockChat as GeminiChat ;
1789+
1790+ const initialRequest = [ { text : 'Hi' } ] ;
1791+ const promptId = 'prompt-id-infinite-invalid-stream' ;
1792+ const signal = new AbortController ( ) . signal ;
1793+
1794+ // Act
1795+ const stream = client . sendMessageStream ( initialRequest , signal , promptId ) ;
1796+ const events = await fromAsync ( stream ) ;
1797+
1798+ // Assert
1799+ // We expect 2 InvalidStream events (original + 1 retry)
1800+ expect ( events . length ) . toBe ( 2 ) ;
1801+ expect (
1802+ events . every ( ( e ) => e . type === GeminiEventType . InvalidStream ) ,
1803+ ) . toBe ( true ) ;
1804+
1805+ // Verify that turn.run was called twice
1806+ expect ( mockTurnRunFn ) . toHaveBeenCalledTimes ( 2 ) ;
1807+ } ) ;
1808+
16801809 describe ( 'Editor context delta' , ( ) => {
16811810 const mockStream = ( async function * ( ) {
16821811 yield { type : 'content' , value : 'Hello' } ;
0 commit comments