@@ -69,15 +69,12 @@ vi.mock('../fallback/handler.js', () => ({
6969 handleFallback : mockHandleFallback ,
7070} ) ) ;
7171
72- const { mockLogInvalidChunk, mockLogContentRetry, mockLogContentRetryFailure } =
73- vi . hoisted ( ( ) => ( {
74- mockLogInvalidChunk : vi . fn ( ) ,
75- mockLogContentRetry : vi . fn ( ) ,
76- mockLogContentRetryFailure : vi . fn ( ) ,
77- } ) ) ;
72+ const { mockLogContentRetry, mockLogContentRetryFailure } = vi . hoisted ( ( ) => ( {
73+ mockLogContentRetry : vi . fn ( ) ,
74+ mockLogContentRetryFailure : vi . fn ( ) ,
75+ } ) ) ;
7876
7977vi . mock ( '../telemetry/loggers.js' , ( ) => ( {
80- logInvalidChunk : mockLogInvalidChunk ,
8178 logContentRetry : mockLogContentRetry ,
8279 logContentRetryFailure : mockLogContentRetryFailure ,
8380} ) ) ;
@@ -454,7 +451,7 @@ describe('GeminiChat', () => {
454451 'This is the visible text that should not be lost.' ,
455452 ) ;
456453 } ) ;
457- it ( 'should add a placeholder model turn when a tool call is followed by an empty stream response' , async ( ) => {
454+ it ( 'should throw an error when a tool call is followed by an empty stream response' , async ( ) => {
458455 // 1. Setup: A history where the model has just made a function call.
459456 const initialHistory : Content [ ] = [
460457 {
@@ -503,23 +500,164 @@ describe('GeminiChat', () => {
503500 } ,
504501 'prompt-id-stream-1' ,
505502 ) ;
506- for await ( const _ of stream ) {
507- // This loop consumes the stream to trigger the internal logic.
508- }
509503
510- // 4. Assert: The history should now have four valid, alternating turns.
511- const history = chat . getHistory ( ) ;
512- expect ( history . length ) . toBe ( 4 ) ;
504+ // 4. Assert: The stream processing should throw an EmptyStreamError.
505+ await expect (
506+ ( async ( ) => {
507+ for await ( const _ of stream ) {
508+ // This loop consumes the stream to trigger the internal logic.
509+ }
510+ } ) ( ) ,
511+ ) . rejects . toThrow ( EmptyStreamError ) ;
512+ } ) ;
513+
514+ it ( 'should succeed when there is a tool call without finish reason' , async ( ) => {
515+ // Setup: Stream with tool call but no finish reason
516+ const streamWithToolCall = ( async function * ( ) {
517+ yield {
518+ candidates : [
519+ {
520+ content : {
521+ role : 'model' ,
522+ parts : [
523+ {
524+ functionCall : {
525+ name : 'test_function' ,
526+ args : { param : 'value' } ,
527+ } ,
528+ } ,
529+ ] ,
530+ } ,
531+ // No finishReason
532+ } ,
533+ ] ,
534+ } as unknown as GenerateContentResponse ;
535+ } ) ( ) ;
536+
537+ vi . mocked ( mockContentGenerator . generateContentStream ) . mockResolvedValue (
538+ streamWithToolCall ,
539+ ) ;
540+
541+ const stream = await chat . sendMessageStream (
542+ 'test-model' ,
543+ { message : 'test' } ,
544+ 'prompt-id-1' ,
545+ ) ;
546+
547+ // Should not throw an error
548+ await expect (
549+ ( async ( ) => {
550+ for await ( const _ of stream ) {
551+ // consume stream
552+ }
553+ } ) ( ) ,
554+ ) . resolves . not . toThrow ( ) ;
555+ } ) ;
556+
557+ it ( 'should throw EmptyStreamError when no tool call and no finish reason' , async ( ) => {
558+ // Setup: Stream with text but no finish reason and no tool call
559+ const streamWithoutFinishReason = ( async function * ( ) {
560+ yield {
561+ candidates : [
562+ {
563+ content : {
564+ role : 'model' ,
565+ parts : [ { text : 'some response' } ] ,
566+ } ,
567+ // No finishReason
568+ } ,
569+ ] ,
570+ } as unknown as GenerateContentResponse ;
571+ } ) ( ) ;
572+
573+ vi . mocked ( mockContentGenerator . generateContentStream ) . mockResolvedValue (
574+ streamWithoutFinishReason ,
575+ ) ;
513576
514- // The final turn must be the empty model placeholder.
515- const lastTurn = history [ 3 ] ! ;
516- expect ( lastTurn . role ) . toBe ( 'model' ) ;
517- expect ( lastTurn ?. parts ?. length ) . toBe ( 0 ) ;
577+ const stream = await chat . sendMessageStream (
578+ 'test-model' ,
579+ { message : 'test' } ,
580+ 'prompt-id-1' ,
581+ ) ;
518582
519- // The second-to-last turn must be the function response we sent.
520- const secondToLastTurn = history [ 2 ] ! ;
521- expect ( secondToLastTurn . role ) . toBe ( 'user' ) ;
522- expect ( secondToLastTurn ?. parts ! [ 0 ] ! . functionResponse ) . toBeDefined ( ) ;
583+ await expect (
584+ ( async ( ) => {
585+ for await ( const _ of stream ) {
586+ // consume stream
587+ }
588+ } ) ( ) ,
589+ ) . rejects . toThrow ( EmptyStreamError ) ;
590+ } ) ;
591+
592+ it ( 'should throw EmptyStreamError when no tool call and empty response text' , async ( ) => {
593+ // Setup: Stream with finish reason but empty response (only thoughts)
594+ const streamWithEmptyResponse = ( async function * ( ) {
595+ yield {
596+ candidates : [
597+ {
598+ content : {
599+ role : 'model' ,
600+ parts : [ { thought : 'thinking...' } ] ,
601+ } ,
602+ finishReason : 'STOP' ,
603+ } ,
604+ ] ,
605+ } as unknown as GenerateContentResponse ;
606+ } ) ( ) ;
607+
608+ vi . mocked ( mockContentGenerator . generateContentStream ) . mockResolvedValue (
609+ streamWithEmptyResponse ,
610+ ) ;
611+
612+ const stream = await chat . sendMessageStream (
613+ 'test-model' ,
614+ { message : 'test' } ,
615+ 'prompt-id-1' ,
616+ ) ;
617+
618+ await expect (
619+ ( async ( ) => {
620+ for await ( const _ of stream ) {
621+ // consume stream
622+ }
623+ } ) ( ) ,
624+ ) . rejects . toThrow ( EmptyStreamError ) ;
625+ } ) ;
626+
627+ it ( 'should succeed when there is finish reason and response text' , async ( ) => {
628+ // Setup: Stream with both finish reason and text content
629+ const validStream = ( async function * ( ) {
630+ yield {
631+ candidates : [
632+ {
633+ content : {
634+ role : 'model' ,
635+ parts : [ { text : 'valid response' } ] ,
636+ } ,
637+ finishReason : 'STOP' ,
638+ } ,
639+ ] ,
640+ } as unknown as GenerateContentResponse ;
641+ } ) ( ) ;
642+
643+ vi . mocked ( mockContentGenerator . generateContentStream ) . mockResolvedValue (
644+ validStream ,
645+ ) ;
646+
647+ const stream = await chat . sendMessageStream (
648+ 'test-model' ,
649+ { message : 'test' } ,
650+ 'prompt-id-1' ,
651+ ) ;
652+
653+ // Should not throw an error
654+ await expect (
655+ ( async ( ) => {
656+ for await ( const _ of stream ) {
657+ // consume stream
658+ }
659+ } ) ( ) ,
660+ ) . resolves . not . toThrow ( ) ;
523661 } ) ;
524662
525663 it ( 'should call generateContentStream with the correct parameters' , async ( ) => {
@@ -690,7 +828,6 @@ describe('GeminiChat', () => {
690828 }
691829
692830 // Assertions
693- expect ( mockLogInvalidChunk ) . toHaveBeenCalledTimes ( 1 ) ;
694831 expect ( mockLogContentRetry ) . toHaveBeenCalledTimes ( 1 ) ;
695832 expect ( mockLogContentRetryFailure ) . not . toHaveBeenCalled ( ) ;
696833 expect ( mockContentGenerator . generateContentStream ) . toHaveBeenCalledTimes (
@@ -758,7 +895,6 @@ describe('GeminiChat', () => {
758895 expect ( mockContentGenerator . generateContentStream ) . toHaveBeenCalledTimes (
759896 3 ,
760897 ) ;
761- expect ( mockLogInvalidChunk ) . toHaveBeenCalledTimes ( 3 ) ;
762898 expect ( mockLogContentRetry ) . toHaveBeenCalledTimes ( 2 ) ;
763899 expect ( mockLogContentRetryFailure ) . toHaveBeenCalledTimes ( 1 ) ;
764900
0 commit comments