From 4c3532d2b395859fb0d9db00bc5209f160ce2e29 Mon Sep 17 00:00:00 2001
From: darkcocoa <67269224+darkdarkcocoa@users.noreply.github.com>
Date: Tue, 22 Jul 2025 06:57:11 +0900
Subject: fix: Add warning message for token limit truncation (#2260)

Co-authored-by: Sandy Tao <sandytao520@icloud.com>
---
 packages/cli/src/ui/hooks/useGeminiStream.test.tsx | 238 ++++++++++++++++++++-
 1 file changed, 237 insertions(+), 1 deletion(-)

(limited to 'packages/cli/src/ui/hooks/useGeminiStream.test.tsx')

diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx
index 7e45cab2..d7fd35c8 100644
--- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx
+++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx
@@ -16,7 +16,12 @@ import {
   TrackedExecutingToolCall,
   TrackedCancelledToolCall,
 } from './useReactToolScheduler.js';
-import { Config, EditorType, AuthType } from '@google/gemini-cli-core';
+import {
+  Config,
+  EditorType,
+  AuthType,
+  GeminiEventType as ServerGeminiEventType,
+} from '@google/gemini-cli-core';
 import { Part, PartListUnion } from '@google/genai';
 import { UseHistoryManagerReturn } from './useHistoryManager.js';
 import {
@@ -1178,4 +1183,235 @@ describe('useGeminiStream', () => {
       });
     });
   });
+
+  describe('handleFinishedEvent', () => {
+    it('should add info message for MAX_TOKENS finish reason', async () => {
+      // Setup mock to return a stream with MAX_TOKENS finish reason
+      mockSendMessageStream.mockReturnValue(
+        (async function* () {
+          yield {
+            type: ServerGeminiEventType.Content,
+            value: 'This is a truncated response...',
+          };
+          yield { type: ServerGeminiEventType.Finished, value: 'MAX_TOKENS' };
+        })(),
+      );
+
+      const { result } = renderHook(() =>
+        useGeminiStream(
+          new MockedGeminiClientClass(mockConfig),
+          [],
+          mockAddItem,
+          mockSetShowHelp,
+          mockConfig,
+          mockOnDebugMessage,
+          mockHandleSlashCommand,
+          false,
+          () => 'vscode' as EditorType,
+          () => {},
+          () => Promise.resolve(),
+          false,
+          () => {},
+        ),
+      );
+
+      // Submit a query
+      await act(async () => {
+        await result.current.submitQuery('Generate long text');
+      });
+
+      // Check that the info message was added
+      await waitFor(() => {
+        expect(mockAddItem).toHaveBeenCalledWith(
+          {
+            type: 'info',
+            text: '⚠️  Response truncated due to token limits.',
+          },
+          expect.any(Number),
+        );
+      });
+    });
+
+    it('should not add message for STOP finish reason', async () => {
+      // Setup mock to return a stream with STOP finish reason
+      mockSendMessageStream.mockReturnValue(
+        (async function* () {
+          yield {
+            type: ServerGeminiEventType.Content,
+            value: 'Complete response',
+          };
+          yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
+        })(),
+      );
+
+      const { result } = renderHook(() =>
+        useGeminiStream(
+          new MockedGeminiClientClass(mockConfig),
+          [],
+          mockAddItem,
+          mockSetShowHelp,
+          mockConfig,
+          mockOnDebugMessage,
+          mockHandleSlashCommand,
+          false,
+          () => 'vscode' as EditorType,
+          () => {},
+          () => Promise.resolve(),
+          false,
+          () => {},
+        ),
+      );
+
+      // Submit a query
+      await act(async () => {
+        await result.current.submitQuery('Test normal completion');
+      });
+
+      // Wait a bit to ensure no message is added
+      await new Promise((resolve) => setTimeout(resolve, 100));
+
+      // Check that no info message was added for STOP
+      const infoMessages = mockAddItem.mock.calls.filter(
+        (call) => call[0].type === 'info',
+      );
+      expect(infoMessages).toHaveLength(0);
+    });
+
+    it('should not add message for FINISH_REASON_UNSPECIFIED', async () => {
+      // Setup mock to return a stream with FINISH_REASON_UNSPECIFIED
+      mockSendMessageStream.mockReturnValue(
+        (async function* () {
+          yield {
+            type: ServerGeminiEventType.Content,
+            value: 'Response with unspecified finish',
+          };
+          yield {
+            type: ServerGeminiEventType.Finished,
+            value: 'FINISH_REASON_UNSPECIFIED',
+          };
+        })(),
+      );
+
+      const { result } = renderHook(() =>
+        useGeminiStream(
+          new MockedGeminiClientClass(mockConfig),
+          [],
+          mockAddItem,
+          mockSetShowHelp,
+          mockConfig,
+          mockOnDebugMessage,
+          mockHandleSlashCommand,
+          false,
+          () => 'vscode' as EditorType,
+          () => {},
+          () => Promise.resolve(),
+          false,
+          () => {},
+        ),
+      );
+
+      // Submit a query
+      await act(async () => {
+        await result.current.submitQuery('Test unspecified finish');
+      });
+
+      // Wait a bit to ensure no message is added
+      await new Promise((resolve) => setTimeout(resolve, 100));
+
+      // Check that no info message was added
+      const infoMessages = mockAddItem.mock.calls.filter(
+        (call) => call[0].type === 'info',
+      );
+      expect(infoMessages).toHaveLength(0);
+    });
+
+    it('should add appropriate messages for other finish reasons', async () => {
+      const testCases = [
+        {
+          reason: 'SAFETY',
+          message: '⚠️  Response stopped due to safety reasons.',
+        },
+        {
+          reason: 'RECITATION',
+          message: '⚠️  Response stopped due to recitation policy.',
+        },
+        {
+          reason: 'LANGUAGE',
+          message: '⚠️  Response stopped due to unsupported language.',
+        },
+        {
+          reason: 'BLOCKLIST',
+          message: '⚠️  Response stopped due to forbidden terms.',
+        },
+        {
+          reason: 'PROHIBITED_CONTENT',
+          message: '⚠️  Response stopped due to prohibited content.',
+        },
+        {
+          reason: 'SPII',
+          message:
+            '⚠️  Response stopped due to sensitive personally identifiable information.',
+        },
+        { reason: 'OTHER', message: '⚠️  Response stopped for other reasons.' },
+        {
+          reason: 'MALFORMED_FUNCTION_CALL',
+          message: '⚠️  Response stopped due to malformed function call.',
+        },
+        {
+          reason: 'IMAGE_SAFETY',
+          message: '⚠️  Response stopped due to image safety violations.',
+        },
+        {
+          reason: 'UNEXPECTED_TOOL_CALL',
+          message: '⚠️  Response stopped due to unexpected tool call.',
+        },
+      ];
+
+      for (const { reason, message } of testCases) {
+        // Reset mocks for each test case
+        mockAddItem.mockClear();
+        mockSendMessageStream.mockReturnValue(
+          (async function* () {
+            yield {
+              type: ServerGeminiEventType.Content,
+              value: `Response for ${reason}`,
+            };
+            yield { type: ServerGeminiEventType.Finished, value: reason };
+          })(),
+        );
+
+        const { result } = renderHook(() =>
+          useGeminiStream(
+            new MockedGeminiClientClass(mockConfig),
+            [],
+            mockAddItem,
+            mockSetShowHelp,
+            mockConfig,
+            mockOnDebugMessage,
+            mockHandleSlashCommand,
+            false,
+            () => 'vscode' as EditorType,
+            () => {},
+            () => Promise.resolve(),
+            false,
+            () => {},
+          ),
+        );
+
+        await act(async () => {
+          await result.current.submitQuery(`Test ${reason}`);
+        });
+
+        await waitFor(() => {
+          expect(mockAddItem).toHaveBeenCalledWith(
+            {
+              type: 'info',
+              text: message,
+            },
+            expect.any(Number),
+          );
+        });
+      }
+    });
+  });
 });
-- 
cgit v1.2.3

