Heray-Was-Here
Server : Apache
System : Linux vps103298.mylogin.co 4.18.0-513.11.1.el8_9.x86_64 #1 SMP Wed Jan 17 02:00:40 EST 2024 x86_64
User : calvet ( 273824)
PHP Version : 7.4.33
Disable Function : NONE
Directory :  /usr/local/lib/node_modules/@google/gemini-cli/dist/src/ui/hooks/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : //usr/local/lib/node_modules/@google/gemini-cli/dist/src/ui/hooks/useGeminiStream.test.js
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useGeminiStream, mergePartListUnions } from './useGeminiStream.js';
import { useInput } from 'ink';
import { useReactToolScheduler, } from './useReactToolScheduler.js';
import { AuthType } from '@google/gemini-cli-core';
import { MessageType, StreamingState } from '../types.js';
// --- MOCKS ---
const mockSendMessageStream = vi
    .fn()
    .mockReturnValue((async function* () { })());
const mockStartChat = vi.fn();
const MockedGeminiClientClass = vi.hoisted(() => vi.fn().mockImplementation(function (_config) {
    // _config
    this.startChat = mockStartChat;
    this.sendMessageStream = mockSendMessageStream;
    this.addHistory = vi.fn();
}));
const MockedUserPromptEvent = vi.hoisted(() => vi.fn().mockImplementation(() => { }));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
    const actualCoreModule = (await importOriginal());
    return {
        ...actualCoreModule,
        GitService: vi.fn(),
        GeminiClient: MockedGeminiClientClass,
        UserPromptEvent: MockedUserPromptEvent,
    };
});
const mockUseReactToolScheduler = useReactToolScheduler;
vi.mock('./useReactToolScheduler.js', async (importOriginal) => {
    const actualSchedulerModule = (await importOriginal());
    return {
        ...(actualSchedulerModule || {}),
        useReactToolScheduler: vi.fn(),
    };
});
vi.mock('ink', async (importOriginal) => {
    const actualInkModule = (await importOriginal());
    return { ...(actualInkModule || {}), useInput: vi.fn() };
});
vi.mock('./shellCommandProcessor.js', () => ({
    useShellCommandProcessor: vi.fn().mockReturnValue({
        handleShellCommand: vi.fn(),
    }),
}));
vi.mock('./atCommandProcessor.js', () => ({
    handleAtCommand: vi
        .fn()
        .mockResolvedValue({ shouldProceed: true, processedQuery: 'mocked' }),
}));
vi.mock('../utils/markdownUtilities.js', () => ({
    findLastSafeSplitPoint: vi.fn((s) => s.length),
}));
vi.mock('./useStateAndRef.js', () => ({
    useStateAndRef: vi.fn((initial) => {
        let val = initial;
        const ref = { current: val };
        const setVal = vi.fn((updater) => {
            if (typeof updater === 'function') {
                val = updater(val);
            }
            else {
                val = updater;
            }
            ref.current = val;
        });
        return [ref, setVal];
    }),
}));
vi.mock('./useLogger.js', () => ({
    useLogger: vi.fn().mockReturnValue({
        logMessage: vi.fn().mockResolvedValue(undefined),
    }),
}));
const mockStartNewTurn = vi.fn();
const mockAddUsage = vi.fn();
vi.mock('../contexts/SessionContext.js', () => ({
    useSessionStats: vi.fn(() => ({
        startNewTurn: mockStartNewTurn,
        addUsage: mockAddUsage,
    })),
}));
vi.mock('./slashCommandProcessor.js', () => ({
    handleSlashCommand: vi.fn().mockReturnValue(false),
}));
const mockParseAndFormatApiError = vi.hoisted(() => vi.fn());
vi.mock('../utils/errorParsing.js', () => ({
    parseAndFormatApiError: mockParseAndFormatApiError,
}));
// --- END MOCKS ---
describe('mergePartListUnions', () => {
    it('should merge multiple PartListUnion arrays', () => {
        const list1 = [{ text: 'Hello' }];
        const list2 = [
            { inlineData: { mimeType: 'image/png', data: 'abc' } },
        ];
        const list3 = [{ text: 'World' }, { text: '!' }];
        const result = mergePartListUnions([list1, list2, list3]);
        expect(result).toEqual([
            { text: 'Hello' },
            { inlineData: { mimeType: 'image/png', data: 'abc' } },
            { text: 'World' },
            { text: '!' },
        ]);
    });
    it('should handle empty arrays in the input list', () => {
        const list1 = [{ text: 'First' }];
        const list2 = [];
        const list3 = [{ text: 'Last' }];
        const result = mergePartListUnions([list1, list2, list3]);
        expect(result).toEqual([{ text: 'First' }, { text: 'Last' }]);
    });
    it('should handle a single PartListUnion array', () => {
        const list1 = [
            { text: 'One' },
            { inlineData: { mimeType: 'image/jpeg', data: 'xyz' } },
        ];
        const result = mergePartListUnions([list1]);
        expect(result).toEqual(list1);
    });
    it('should return an empty array if all input arrays are empty', () => {
        const list1 = [];
        const list2 = [];
        const result = mergePartListUnions([list1, list2]);
        expect(result).toEqual([]);
    });
    it('should handle input list being empty', () => {
        const result = mergePartListUnions([]);
        expect(result).toEqual([]);
    });
    it('should correctly merge when PartListUnion items are single Parts not in arrays', () => {
        const part1 = { text: 'Single part 1' };
        const part2 = { inlineData: { mimeType: 'image/gif', data: 'gif' } };
        const listContainingSingleParts = [
            part1,
            [part2],
            { text: 'Another single part' },
        ];
        const result = mergePartListUnions(listContainingSingleParts);
        expect(result).toEqual([
            { text: 'Single part 1' },
            { inlineData: { mimeType: 'image/gif', data: 'gif' } },
            { text: 'Another single part' },
        ]);
    });
    it('should handle a mix of arrays and single parts, including empty arrays and undefined/null parts if they were possible (though PartListUnion typing restricts this)', () => {
        const list1 = [{ text: 'A' }];
        const list2 = [];
        const part3 = { text: 'B' };
        const list4 = [
            { text: 'C' },
            { inlineData: { mimeType: 'text/plain', data: 'D' } },
        ];
        const result = mergePartListUnions([list1, list2, part3, list4]);
        expect(result).toEqual([
            { text: 'A' },
            { text: 'B' },
            { text: 'C' },
            { inlineData: { mimeType: 'text/plain', data: 'D' } },
        ]);
    });
    it('should preserve the order of parts from the input arrays', () => {
        const listA = [{ text: '1' }, { text: '2' }];
        const listB = [{ text: '3' }];
        const listC = [{ text: '4' }, { text: '5' }];
        const result = mergePartListUnions([listA, listB, listC]);
        expect(result).toEqual([
            { text: '1' },
            { text: '2' },
            { text: '3' },
            { text: '4' },
            { text: '5' },
        ]);
    });
    it('should handle cases where some PartListUnion items are single Parts and others are arrays of Parts', () => {
        const singlePart1 = { text: 'First single' };
        const arrayPart1 = [
            { text: 'Array item 1' },
            { text: 'Array item 2' },
        ];
        const singlePart2 = {
            inlineData: { mimeType: 'application/json', data: 'e30=' },
        }; // {}
        const arrayPart2 = [{ text: 'Last array item' }];
        const result = mergePartListUnions([
            singlePart1,
            arrayPart1,
            singlePart2,
            arrayPart2,
        ]);
        expect(result).toEqual([
            { text: 'First single' },
            { text: 'Array item 1' },
            { text: 'Array item 2' },
            { inlineData: { mimeType: 'application/json', data: 'e30=' } },
            { text: 'Last array item' },
        ]);
    });
});
// --- Tests for useGeminiStream Hook ---
describe('useGeminiStream', () => {
    let mockAddItem;
    let mockSetShowHelp;
    let mockConfig;
    let mockOnDebugMessage;
    let mockHandleSlashCommand;
    let mockScheduleToolCalls;
    let mockCancelAllToolCalls;
    let mockMarkToolsAsSubmitted;
    beforeEach(() => {
        vi.clearAllMocks(); // Clear mocks before each test
        mockAddItem = vi.fn();
        mockSetShowHelp = vi.fn();
        // Define the mock for getGeminiClient
        const mockGetGeminiClient = vi.fn().mockImplementation(() => {
            // MockedGeminiClientClass is defined in the module scope by the previous change.
            // It will use the mockStartChat and mockSendMessageStream that are managed within beforeEach.
            const clientInstance = new MockedGeminiClientClass(mockConfig);
            return clientInstance;
        });
        mockConfig = {
            apiKey: 'test-api-key',
            model: 'gemini-pro',
            sandbox: false,
            targetDir: '/test/dir',
            debugMode: false,
            question: undefined,
            fullContext: false,
            coreTools: [],
            toolDiscoveryCommand: undefined,
            toolCallCommand: undefined,
            mcpServerCommand: undefined,
            mcpServers: undefined,
            userAgent: 'test-agent',
            userMemory: '',
            geminiMdFileCount: 0,
            alwaysSkipModificationConfirmation: false,
            vertexai: false,
            showMemoryUsage: false,
            contextFileName: undefined,
            getToolRegistry: vi.fn(() => ({ getToolSchemaList: vi.fn(() => []) })),
            getProjectRoot: vi.fn(() => '/test/dir'),
            getCheckpointingEnabled: vi.fn(() => false),
            getGeminiClient: mockGetGeminiClient,
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            addHistory: vi.fn(),
        };
        mockOnDebugMessage = vi.fn();
        mockHandleSlashCommand = vi.fn().mockResolvedValue(false);
        // Mock return value for useReactToolScheduler
        mockScheduleToolCalls = vi.fn();
        mockCancelAllToolCalls = vi.fn();
        mockMarkToolsAsSubmitted = vi.fn();
        // Default mock for useReactToolScheduler to prevent toolCalls being undefined initially
        mockUseReactToolScheduler.mockReturnValue([
            [], // Default to empty array for toolCalls
            mockScheduleToolCalls,
            mockCancelAllToolCalls,
            mockMarkToolsAsSubmitted,
        ]);
        // Reset mocks for GeminiClient instance methods (startChat and sendMessageStream)
        // The GeminiClient constructor itself is mocked at the module level.
        mockStartChat.mockClear().mockResolvedValue({
            sendMessageStream: mockSendMessageStream,
        }); // GeminiChat -> any
        mockSendMessageStream
            .mockClear()
            .mockReturnValue((async function* () { })());
    });
    const mockLoadedSettings = {
        merged: { preferredEditor: 'vscode' },
        user: { path: '/user/settings.json', settings: {} },
        workspace: { path: '/workspace/.gemini/settings.json', settings: {} },
        errors: [],
        forScope: vi.fn(),
        setValue: vi.fn(),
    };
    const renderTestHook = (initialToolCalls = [], geminiClient) => {
        let currentToolCalls = initialToolCalls;
        const setToolCalls = (newToolCalls) => {
            currentToolCalls = newToolCalls;
        };
        mockUseReactToolScheduler.mockImplementation(() => [
            currentToolCalls,
            mockScheduleToolCalls,
            mockCancelAllToolCalls,
            mockMarkToolsAsSubmitted,
        ]);
        const client = geminiClient || mockConfig.getGeminiClient();
        const { result, rerender } = renderHook((props) => {
            // Update the mock's return value if new toolCalls are passed in props
            if (props.toolCalls) {
                setToolCalls(props.toolCalls);
            }
            return useGeminiStream(props.client, props.history, props.addItem, props.setShowHelp, props.config, props.onDebugMessage, props.handleSlashCommand, props.shellModeActive, () => 'vscode', () => { }, () => Promise.resolve());
        }, {
            initialProps: {
                client,
                history: [],
                addItem: mockAddItem,
                setShowHelp: mockSetShowHelp,
                config: mockConfig,
                onDebugMessage: mockOnDebugMessage,
                handleSlashCommand: mockHandleSlashCommand,
                shellModeActive: false,
                loadedSettings: mockLoadedSettings,
                toolCalls: initialToolCalls,
            },
        });
        return {
            result,
            rerender,
            mockMarkToolsAsSubmitted,
            mockSendMessageStream,
            client,
        };
    };
    it('should not submit tool responses if not all tool calls are completed', () => {
        const toolCalls = [
            {
                request: {
                    callId: 'call1',
                    name: 'tool1',
                    args: {},
                    isClientInitiated: false,
                },
                status: 'success',
                responseSubmittedToGemini: false,
                response: {
                    callId: 'call1',
                    responseParts: [{ text: 'tool 1 response' }],
                    error: undefined,
                    resultDisplay: 'Tool 1 success display',
                },
                tool: {
                    name: 'tool1',
                    description: 'desc1',
                    getDescription: vi.fn(),
                },
                startTime: Date.now(),
                endTime: Date.now(),
            },
            {
                request: { callId: 'call2', name: 'tool2', args: {} },
                status: 'executing',
                responseSubmittedToGemini: false,
                tool: {
                    name: 'tool2',
                    description: 'desc2',
                    getDescription: vi.fn(),
                },
                startTime: Date.now(),
                liveOutput: '...',
            },
        ];
        const { mockMarkToolsAsSubmitted, mockSendMessageStream } = renderTestHook(toolCalls);
        // Effect for submitting tool responses depends on toolCalls and isResponding
        // isResponding is initially false, so the effect should run.
        expect(mockMarkToolsAsSubmitted).not.toHaveBeenCalled();
        expect(mockSendMessageStream).not.toHaveBeenCalled(); // submitQuery uses this
    });
    it('should submit tool responses when all tool calls are completed and ready', async () => {
        const toolCall1ResponseParts = [
            { text: 'tool 1 final response' },
        ];
        const toolCall2ResponseParts = [
            { text: 'tool 2 final response' },
        ];
        const completedToolCalls = [
            {
                request: {
                    callId: 'call1',
                    name: 'tool1',
                    args: {},
                    isClientInitiated: false,
                },
                status: 'success',
                responseSubmittedToGemini: false,
                response: { callId: 'call1', responseParts: toolCall1ResponseParts },
            },
            {
                request: {
                    callId: 'call2',
                    name: 'tool2',
                    args: {},
                    isClientInitiated: false,
                },
                status: 'error',
                responseSubmittedToGemini: false,
                response: { callId: 'call2', responseParts: toolCall2ResponseParts },
            },
        ];
        // Capture the onComplete callback
        let capturedOnComplete = null;
        mockUseReactToolScheduler.mockImplementation((onComplete) => {
            capturedOnComplete = onComplete;
            return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
        });
        renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockSetShowHelp, mockConfig, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve()));
        // Trigger the onComplete callback with completed tools
        await act(async () => {
            if (capturedOnComplete) {
                await capturedOnComplete(completedToolCalls);
            }
        });
        await waitFor(() => {
            expect(mockMarkToolsAsSubmitted).toHaveBeenCalledTimes(1);
            expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
        });
        const expectedMergedResponse = mergePartListUnions([
            toolCall1ResponseParts,
            toolCall2ResponseParts,
        ]);
        expect(mockSendMessageStream).toHaveBeenCalledWith(expectedMergedResponse, expect.any(AbortSignal));
    });
    it('should handle all tool calls being cancelled', async () => {
        const cancelledToolCalls = [
            {
                request: {
                    callId: '1',
                    name: 'testTool',
                    args: {},
                    isClientInitiated: false,
                },
                status: 'cancelled',
                response: { callId: '1', responseParts: [{ text: 'cancelled' }] },
                responseSubmittedToGemini: false,
            },
        ];
        const client = new MockedGeminiClientClass(mockConfig);
        // Capture the onComplete callback
        let capturedOnComplete = null;
        mockUseReactToolScheduler.mockImplementation((onComplete) => {
            capturedOnComplete = onComplete;
            return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
        });
        renderHook(() => useGeminiStream(client, [], mockAddItem, mockSetShowHelp, mockConfig, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve()));
        // Trigger the onComplete callback with cancelled tools
        await act(async () => {
            if (capturedOnComplete) {
                await capturedOnComplete(cancelledToolCalls);
            }
        });
        await waitFor(() => {
            expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith(['1']);
            expect(client.addHistory).toHaveBeenCalledWith({
                role: 'user',
                parts: [{ text: 'cancelled' }],
            });
            // Ensure we do NOT call back to the API
            expect(mockSendMessageStream).not.toHaveBeenCalled();
        });
    });
    it('should not flicker streaming state to Idle between tool completion and submission', async () => {
        const toolCallResponseParts = [
            { text: 'tool 1 final response' },
        ];
        const initialToolCalls = [
            {
                request: {
                    callId: 'call1',
                    name: 'tool1',
                    args: {},
                    isClientInitiated: false,
                },
                status: 'executing',
                responseSubmittedToGemini: false,
                tool: {
                    name: 'tool1',
                    description: 'desc',
                    getDescription: vi.fn(),
                },
                startTime: Date.now(),
            },
        ];
        const completedToolCalls = [
            {
                ...initialToolCalls[0],
                status: 'success',
                response: {
                    callId: 'call1',
                    responseParts: toolCallResponseParts,
                    error: undefined,
                    resultDisplay: 'Tool 1 success display',
                },
                endTime: Date.now(),
            },
        ];
        // Capture the onComplete callback
        let capturedOnComplete = null;
        let currentToolCalls = initialToolCalls;
        mockUseReactToolScheduler.mockImplementation((onComplete) => {
            capturedOnComplete = onComplete;
            return [
                currentToolCalls,
                mockScheduleToolCalls,
                mockMarkToolsAsSubmitted,
            ];
        });
        const { result, rerender } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockSetShowHelp, mockConfig, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve()));
        // 1. Initial state should be Responding because a tool is executing.
        expect(result.current.streamingState).toBe(StreamingState.Responding);
        // 2. Update the tool calls to completed state and rerender
        currentToolCalls = completedToolCalls;
        mockUseReactToolScheduler.mockImplementation((onComplete) => {
            capturedOnComplete = onComplete;
            return [
                completedToolCalls,
                mockScheduleToolCalls,
                mockMarkToolsAsSubmitted,
            ];
        });
        act(() => {
            rerender();
        });
        // 3. The state should *still* be Responding, not Idle.
        // This is because the completed tool's response has not been submitted yet.
        expect(result.current.streamingState).toBe(StreamingState.Responding);
        // 4. Trigger the onComplete callback to simulate tool completion
        await act(async () => {
            if (capturedOnComplete) {
                await capturedOnComplete(completedToolCalls);
            }
        });
        // 5. Wait for submitQuery to be called
        await waitFor(() => {
            expect(mockSendMessageStream).toHaveBeenCalledWith(toolCallResponseParts, expect.any(AbortSignal));
        });
        // 6. After submission, the state should remain Responding until the stream completes.
        expect(result.current.streamingState).toBe(StreamingState.Responding);
    });
    describe('User Cancellation', () => {
        let useInputCallback;
        const mockUseInput = useInput;
        beforeEach(() => {
            // Capture the callback passed to useInput
            mockUseInput.mockImplementation((callback) => {
                useInputCallback = callback;
            });
        });
        const simulateEscapeKeyPress = () => {
            act(() => {
                useInputCallback('', { escape: true });
            });
        };
        it('should cancel an in-progress stream when escape is pressed', async () => {
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Part 1' };
                // Keep the stream open
                await new Promise(() => { });
            })();
            mockSendMessageStream.mockReturnValue(mockStream);
            const { result } = renderTestHook();
            // Start a query
            await act(async () => {
                result.current.submitQuery('test query');
            });
            // Wait for the first part of the response
            await waitFor(() => {
                expect(result.current.streamingState).toBe(StreamingState.Responding);
            });
            // Simulate escape key press
            simulateEscapeKeyPress();
            // Verify cancellation message is added
            await waitFor(() => {
                expect(mockAddItem).toHaveBeenCalledWith({
                    type: MessageType.INFO,
                    text: 'Request cancelled.',
                }, expect.any(Number));
            });
            // Verify state is reset
            expect(result.current.streamingState).toBe(StreamingState.Idle);
        });
        it('should not do anything if escape is pressed when not responding', () => {
            const { result } = renderTestHook();
            expect(result.current.streamingState).toBe(StreamingState.Idle);
            // Simulate escape key press
            simulateEscapeKeyPress();
            // No change should happen, no cancellation message
            expect(mockAddItem).not.toHaveBeenCalledWith(expect.objectContaining({
                text: 'Request cancelled.',
            }), expect.any(Number));
        });
        it('should prevent further processing after cancellation', async () => {
            let continueStream;
            const streamPromise = new Promise((resolve) => {
                continueStream = resolve;
            });
            const mockStream = (async function* () {
                yield { type: 'content', value: 'Initial' };
                await streamPromise; // Wait until we manually continue
                yield { type: 'content', value: ' Canceled' };
            })();
            mockSendMessageStream.mockReturnValue(mockStream);
            const { result } = renderTestHook();
            await act(async () => {
                result.current.submitQuery('long running query');
            });
            await waitFor(() => {
                expect(result.current.streamingState).toBe(StreamingState.Responding);
            });
            // Cancel the request
            simulateEscapeKeyPress();
            // Allow the stream to continue
            act(() => {
                continueStream();
            });
            // Wait a bit to see if the second part is processed
            await new Promise((resolve) => setTimeout(resolve, 50));
            // The text should not have been updated with " Canceled"
            const lastCall = mockAddItem.mock.calls.find((call) => call[0].type === 'gemini');
            expect(lastCall?.[0].text).toBe('Initial');
            // The final state should be idle after cancellation
            expect(result.current.streamingState).toBe(StreamingState.Idle);
        });
        it('should not cancel if a tool call is in progress (not just responding)', async () => {
            const toolCalls = [
                {
                    request: { callId: 'call1', name: 'tool1', args: {} },
                    status: 'executing',
                    responseSubmittedToGemini: false,
                    tool: {
                        name: 'tool1',
                        description: 'desc1',
                        getDescription: vi.fn(),
                    },
                    startTime: Date.now(),
                    liveOutput: '...',
                },
            ];
            const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
            const { result } = renderTestHook(toolCalls);
            // State is `Responding` because a tool is running
            expect(result.current.streamingState).toBe(StreamingState.Responding);
            // Try to cancel
            simulateEscapeKeyPress();
            // Nothing should happen because the state is not `Responding`
            expect(abortSpy).not.toHaveBeenCalled();
        });
    });
    describe('Client-Initiated Tool Calls', () => {
        it('should execute a client-initiated tool without sending a response to Gemini', async () => {
            const clientToolRequest = {
                shouldScheduleTool: true,
                toolName: 'save_memory',
                toolArgs: { fact: 'test fact' },
            };
            mockHandleSlashCommand.mockResolvedValue(clientToolRequest);
            const completedToolCall = {
                request: {
                    callId: 'client-call-1',
                    name: clientToolRequest.toolName,
                    args: clientToolRequest.toolArgs,
                    isClientInitiated: true,
                },
                status: 'success',
                responseSubmittedToGemini: false,
                response: {
                    callId: 'client-call-1',
                    responseParts: [{ text: 'Memory saved' }],
                    resultDisplay: 'Success: Memory saved',
                    error: undefined,
                },
                tool: {
                    name: clientToolRequest.toolName,
                    description: 'Saves memory',
                    getDescription: vi.fn(),
                },
            };
            // Capture the onComplete callback
            let capturedOnComplete = null;
            mockUseReactToolScheduler.mockImplementation((onComplete) => {
                capturedOnComplete = onComplete;
                return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
            });
            const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockSetShowHelp, mockConfig, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve()));
            // --- User runs the slash command ---
            await act(async () => {
                await result.current.submitQuery('/memory add "test fact"');
            });
            // Trigger the onComplete callback with the completed client-initiated tool
            await act(async () => {
                if (capturedOnComplete) {
                    await capturedOnComplete([completedToolCall]);
                }
            });
            // --- Assert the outcome ---
            await waitFor(() => {
                // The tool should be marked as submitted locally
                expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith([
                    'client-call-1',
                ]);
                // Crucially, no message should be sent to the Gemini API
                expect(mockSendMessageStream).not.toHaveBeenCalled();
            });
        });
    });
    describe('Memory Refresh on save_memory', () => {
        it('should call performMemoryRefresh when a save_memory tool call completes successfully', async () => {
            const mockPerformMemoryRefresh = vi.fn();
            const completedToolCall = {
                request: {
                    callId: 'save-mem-call-1',
                    name: 'save_memory',
                    args: { fact: 'test' },
                    isClientInitiated: true,
                },
                status: 'success',
                responseSubmittedToGemini: false,
                response: {
                    callId: 'save-mem-call-1',
                    responseParts: [{ text: 'Memory saved' }],
                    resultDisplay: 'Success: Memory saved',
                    error: undefined,
                },
                tool: {
                    name: 'save_memory',
                    description: 'Saves memory',
                    getDescription: vi.fn(),
                },
            };
            // Capture the onComplete callback
            let capturedOnComplete = null;
            mockUseReactToolScheduler.mockImplementation((onComplete) => {
                capturedOnComplete = onComplete;
                return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
            });
            renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockSetShowHelp, mockConfig, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, mockPerformMemoryRefresh));
            // Trigger the onComplete callback with the completed save_memory tool
            await act(async () => {
                if (capturedOnComplete) {
                    await capturedOnComplete([completedToolCall]);
                }
            });
            await waitFor(() => {
                expect(mockPerformMemoryRefresh).toHaveBeenCalledTimes(1);
            });
        });
    });
    describe('Error Handling', () => {
        it('should call parseAndFormatApiError with the correct authType on stream initialization failure', async () => {
            // 1. Setup
            const mockError = new Error('Rate limit exceeded');
            const mockAuthType = AuthType.LOGIN_WITH_GOOGLE;
            mockParseAndFormatApiError.mockClear();
            mockSendMessageStream.mockReturnValue((async function* () {
                yield { type: 'content', value: '' };
                throw mockError;
            })());
            const testConfig = {
                ...mockConfig,
                getContentGeneratorConfig: vi.fn(() => ({
                    authType: mockAuthType,
                })),
            };
            const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(testConfig), [], mockAddItem, mockSetShowHelp, testConfig, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve()));
            // 2. Action
            await act(async () => {
                await result.current.submitQuery('test query');
            });
            // 3. Assertion
            await waitFor(() => {
                expect(mockParseAndFormatApiError).toHaveBeenCalledWith('Rate limit exceeded', mockAuthType);
            });
        });
    });
});
//# sourceMappingURL=useGeminiStream.test.js.map

Hry