mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 04:48:09 -07:00
Merge branch 'main' into fix/windows-preflight-resilience
This commit is contained in:
@@ -6,8 +6,10 @@
|
||||
|
||||
import type { IdeInfo } from '@google/gemini-cli-core';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { RadioSelectItem } from './components/shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './components/shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from './hooks/useKeypress.js';
|
||||
import { theme } from './semantic-colors.js';
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ import { useCallback, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
||||
import type {
|
||||
LoadableSettingScope,
|
||||
LoadedSettings,
|
||||
import {
|
||||
SettingScope,
|
||||
type LoadableSettingScope,
|
||||
type LoadedSettings,
|
||||
} from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import {
|
||||
AuthType,
|
||||
clearCachedCredentialFile,
|
||||
|
||||
@@ -8,11 +8,11 @@ import type React from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type {
|
||||
LoadableSettingScope,
|
||||
LoadedSettings,
|
||||
import {
|
||||
SettingScope,
|
||||
type LoadableSettingScope,
|
||||
type LoadedSettings,
|
||||
} from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import type { AgentDefinition, AgentOverride } from '@google/gemini-cli-core';
|
||||
import { getCachedStringWidth } from '../utils/textUtils.js';
|
||||
import {
|
||||
|
||||
@@ -15,13 +15,12 @@ import {
|
||||
} from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type { Question } from '@google/gemini-cli-core';
|
||||
import { checkExhaustive, type Question } from '@google/gemini-cli-core';
|
||||
import { BaseSelectionList } from './shared/BaseSelectionList.js';
|
||||
import type { SelectionListItem } from '../hooks/useSelectionList.js';
|
||||
import { TabHeader, type Tab } from './shared/TabHeader.js';
|
||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||
import { Command } from '../key/keyMatchers.js';
|
||||
import { checkExhaustive } from '@google/gemini-cli-core';
|
||||
import { TextInput } from './shared/TextInput.js';
|
||||
import { formatCommand } from '../key/keybindingUtils.js';
|
||||
import {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useMemo } from 'react';
|
||||
import { ChecklistItem, type ChecklistItemData } from './ChecklistItem.js';
|
||||
|
||||
export interface ChecklistProps {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useRef, useCallback } from 'react';
|
||||
import type React from 'react';
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type { ConsoleMessageItem } from '../types.js';
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { EditorSettingsDialog } from './EditorSettingsDialog.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { SettingScope, type LoadedSettings } from '../../config/settings.js';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { act } from 'react';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
|
||||
@@ -13,18 +13,18 @@ import {
|
||||
type EditorDisplay,
|
||||
} from '../editors/editorSettingsManager.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import type {
|
||||
LoadableSettingScope,
|
||||
LoadedSettings,
|
||||
import {
|
||||
SettingScope,
|
||||
type LoadableSettingScope,
|
||||
type LoadedSettings,
|
||||
} from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import {
|
||||
type EditorType,
|
||||
isEditorAvailable,
|
||||
EDITOR_DISPLAY_NAMES,
|
||||
coreEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
|
||||
interface EditorDialogProps {
|
||||
onSelect: (
|
||||
|
||||
@@ -9,8 +9,10 @@ import type React from 'react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { MaxSizedBox } from './shared/MaxSizedBox.js';
|
||||
import { Scrollable } from './shared/Scrollable.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
||||
import { type SessionStatsState } from '../contexts/SessionContext.js';
|
||||
import { Banner } from './Banner.js';
|
||||
import { Footer } from './Footer.js';
|
||||
import { Header } from './Header.js';
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Help } from './Help.js';
|
||||
import type { SlashCommand } from '../commands/types.js';
|
||||
import { CommandKind } from '../commands/types.js';
|
||||
import { CommandKind, type SlashCommand } from '../commands/types.js';
|
||||
|
||||
const mockCommands: readonly SlashCommand[] = [
|
||||
{
|
||||
|
||||
@@ -6,13 +6,12 @@
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
|
||||
import { type HistoryItem } from '../types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { MessageType, type HistoryItem } from '../types.js';
|
||||
import { SessionStatsProvider } from '../contexts/SessionContext.js';
|
||||
import {
|
||||
CoreToolCallStatus,
|
||||
type Config,
|
||||
type ToolExecuteConfirmationDetails,
|
||||
CoreToolCallStatus,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
|
||||
@@ -8,31 +8,46 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { act, useState } from 'react';
|
||||
import type { InputPromptProps } from './InputPrompt.js';
|
||||
import { InputPrompt, tryTogglePasteExpansion } from './InputPrompt.js';
|
||||
import type { TextBuffer } from './shared/text-buffer.js';
|
||||
import {
|
||||
InputPrompt,
|
||||
tryTogglePasteExpansion,
|
||||
type InputPromptProps,
|
||||
} from './InputPrompt.js';
|
||||
import {
|
||||
calculateTransformationsForLine,
|
||||
calculateTransformedLine,
|
||||
type TextBuffer,
|
||||
} from './shared/text-buffer.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { ApprovalMode, debugLogger } from '@google/gemini-cli-core';
|
||||
import {
|
||||
ApprovalMode,
|
||||
debugLogger,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as path from 'node:path';
|
||||
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import { CommandKind } from '../commands/types.js';
|
||||
import {
|
||||
CommandKind,
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
} from '../commands/types.js';
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { Text } from 'ink';
|
||||
import type { UseShellHistoryReturn } from '../hooks/useShellHistory.js';
|
||||
import { useShellHistory } from '../hooks/useShellHistory.js';
|
||||
import type { UseCommandCompletionReturn } from '../hooks/useCommandCompletion.js';
|
||||
import {
|
||||
useShellHistory,
|
||||
type UseShellHistoryReturn,
|
||||
} from '../hooks/useShellHistory.js';
|
||||
import {
|
||||
useCommandCompletion,
|
||||
CompletionMode,
|
||||
type UseCommandCompletionReturn,
|
||||
} from '../hooks/useCommandCompletion.js';
|
||||
import type { UseInputHistoryReturn } from '../hooks/useInputHistory.js';
|
||||
import { useInputHistory } from '../hooks/useInputHistory.js';
|
||||
import type { UseReverseSearchCompletionReturn } from '../hooks/useReverseSearchCompletion.js';
|
||||
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
|
||||
import {
|
||||
useInputHistory,
|
||||
type UseInputHistoryReturn,
|
||||
} from '../hooks/useInputHistory.js';
|
||||
import {
|
||||
useReverseSearchCompletion,
|
||||
type UseReverseSearchCompletionReturn,
|
||||
} from '../hooks/useReverseSearchCompletion.js';
|
||||
import clipboardy from 'clipboardy';
|
||||
import * as clipboardUtils from '../utils/clipboardUtils.js';
|
||||
import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js';
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import clipboardy from 'clipboardy';
|
||||
import { useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import clipboardy from 'clipboardy';
|
||||
import { Box, Text, useStdout, type DOMElement } from 'ink';
|
||||
import { SuggestionsDisplay, MAX_WIDTH } from './SuggestionsDisplay.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
@@ -34,13 +34,16 @@ import {
|
||||
useCommandCompletion,
|
||||
CompletionMode,
|
||||
} from '../hooks/useCommandCompletion.js';
|
||||
import type { Key } from '../hooks/useKeypress.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||
import { Command } from '../key/keyMatchers.js';
|
||||
import { formatCommand } from '../key/keybindingUtils.js';
|
||||
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { ApprovalMode, coreEvents, debugLogger } from '@google/gemini-cli-core';
|
||||
import {
|
||||
ApprovalMode,
|
||||
coreEvents,
|
||||
debugLogger,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
parseInputForHighlighting,
|
||||
parseSegmentsFromTokens,
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
export enum LogoutChoice {
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import * as SettingsContext from '../contexts/SettingsContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { type LoadedSettings } from '../../config/settings.js';
|
||||
import { type SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { ToolCallDecision, LlmRole } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the context to provide controlled data for testing
|
||||
|
||||
@@ -8,14 +8,16 @@ import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { loadTrustedFolders, TrustLevel } from '../../config/trustedFolders.js';
|
||||
import { expandHomeDir } from '../utils/directoryUtils.js';
|
||||
import * as path from 'node:path';
|
||||
import { MessageType, type HistoryItem } from '../types.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { type Config } from '@google/gemini-cli-core';
|
||||
|
||||
export enum MultiFolderTrustChoice {
|
||||
YES,
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { Mock } from 'vitest';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
||||
|
||||
@@ -5,16 +5,18 @@
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import type React from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import {
|
||||
PolicyIntegrityManager,
|
||||
type Config,
|
||||
type PolicyUpdateConfirmationRequest,
|
||||
PolicyIntegrityManager,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { Command } from '../key/keyMatchers.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
@@ -8,8 +8,10 @@ import { Box, Text, useIsScreenReaderEnabled } from 'ink';
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import type { FileChangeStats } from '../utils/rewindFileOps.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { formatTimeAgo } from '../utils/formatters.js';
|
||||
|
||||
@@ -8,10 +8,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { SessionBrowser } from './SessionBrowser.js';
|
||||
import type { SessionBrowserProps } from './SessionBrowser.js';
|
||||
import type { SessionInfo } from '../../utils/sessionUtils.js';
|
||||
import { type Config } from '@google/gemini-cli-core';
|
||||
import { SessionBrowser, type SessionBrowserProps } from './SessionBrowser.js';
|
||||
import { type SessionInfo } from '../../utils/sessionUtils.js';
|
||||
|
||||
// Collect key handlers registered via useKeypress so tests can
|
||||
// simulate input without going through the full stdin pipeline.
|
||||
|
||||
@@ -8,7 +8,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { type SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import {
|
||||
ToolCallDecision,
|
||||
getShellConfiguration,
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import type React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { AsyncFzf } from 'fzf';
|
||||
import type { Key } from '../hooks/useKeypress.js';
|
||||
import { type Key } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type { LoadableSettingScope, Settings } from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import {
|
||||
SettingScope,
|
||||
type LoadableSettingScope,
|
||||
type Settings,
|
||||
} from '../../config/settings.js';
|
||||
import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
|
||||
import {
|
||||
getDialogSettingKeys,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { ShellExecutionService } from '@google/gemini-cli-core';
|
||||
import { keyToAnsi, type Key } from '../key/keyToAnsi.js';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { type SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import {
|
||||
ToolCallDecision,
|
||||
type RetrieveUserQuotaResponse,
|
||||
|
||||
@@ -9,8 +9,10 @@ import { Box, Text, useStdout } from 'ink';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { formatDuration, formatResetTime } from '../utils/formatters.js';
|
||||
import type { ModelMetrics } from '../contexts/SessionContext.js';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import {
|
||||
useSessionStats,
|
||||
type ModelMetrics,
|
||||
} from '../contexts/SessionContext.js';
|
||||
import {
|
||||
getStatusColor,
|
||||
TOOL_SUCCESS_RATE_HIGH,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ToolStatsDisplay } from './ToolStatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { type SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the context to provide controlled data for testing
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import type { CompressionDisplayProps } from './CompressionMessage.js';
|
||||
import { CompressionMessage } from './CompressionMessage.js';
|
||||
import {
|
||||
CompressionMessage,
|
||||
type CompressionDisplayProps,
|
||||
} from './CompressionMessage.js';
|
||||
import { CompressionStatus } from '@google/gemini-cli-core';
|
||||
import type { CompressionProps } from '../../types.js';
|
||||
import { type CompressionProps } from '../../types.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('<CompressionMessage />', () => {
|
||||
|
||||
@@ -8,11 +8,9 @@ import { render } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Box } from 'ink';
|
||||
import { TodoTray } from './Todo.js';
|
||||
import type { Todo } from '@google/gemini-cli-core';
|
||||
import type { UIState } from '../../contexts/UIStateContext.js';
|
||||
import { UIStateContext } from '../../contexts/UIStateContext.js';
|
||||
import type { HistoryItem } from '../../types.js';
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import { CoreToolCallStatus, type Todo } from '@google/gemini-cli-core';
|
||||
import { UIStateContext, type UIState } from '../../contexts/UIStateContext.js';
|
||||
import { type HistoryItem } from '../../types.js';
|
||||
|
||||
const createTodoHistoryItem = (todos: Todo[]): HistoryItem =>
|
||||
({
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { type TodoList } from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { useMemo } from 'react';
|
||||
import type { HistoryItemToolGroup } from '../../types.js';
|
||||
import { Checklist } from '../Checklist.js';
|
||||
import type { ChecklistItemData } from '../ChecklistItem.js';
|
||||
|
||||
@@ -18,9 +18,11 @@ import {
|
||||
hasRedirection,
|
||||
debugLogger,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { RadioSelectItem } from '../shared/RadioButtonSelect.js';
|
||||
import { useToolActions } from '../../contexts/ToolActionsContext.js';
|
||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from '../shared/RadioButtonSelect.js';
|
||||
import { MaxSizedBox, MINIMUM_MAX_HEIGHT } from '../shared/MaxSizedBox.js';
|
||||
import {
|
||||
sanitizeForDisplay,
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { ToolMessageProps } from './ToolMessage.js';
|
||||
import { ToolMessage } from './ToolMessage.js';
|
||||
import { ToolMessage, type ToolMessageProps } from './ToolMessage.js';
|
||||
import { StreamingState } from '../../types.js';
|
||||
import { StreamingContext } from '../../contexts/StreamingContext.js';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
|
||||
@@ -8,9 +8,10 @@ import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useSelectionList } from '../../hooks/useSelectionList.js';
|
||||
|
||||
import type { SelectionListItem } from '../../hooks/useSelectionList.js';
|
||||
import {
|
||||
useSelectionList,
|
||||
type SelectionListItem,
|
||||
} from '../../hooks/useSelectionList.js';
|
||||
|
||||
export interface RenderItemContext {
|
||||
isSelected: boolean;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import type React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../../colors.js';
|
||||
import type { SettingEnumOption } from '../../../config/settingsSchema.js';
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import type { Text } from 'ink';
|
||||
import { Box } from 'ink';
|
||||
import type React from 'react';
|
||||
import { Box, type Text } from 'ink';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { Key } from '../../hooks/useKeypress.js';
|
||||
import { Text, Box } from 'ink';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import chalk from 'chalk';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type { TextBuffer } from './text-buffer.js';
|
||||
import { expandPastePlaceholders } from './text-buffer.js';
|
||||
import { expandPastePlaceholders, type TextBuffer } from './text-buffer.js';
|
||||
import { cpSlice, cpIndexToOffset } from '../../utils/textUtils.js';
|
||||
import { Command } from '../../key/keyMatchers.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Spinner from 'ink-spinner';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core';
|
||||
import {
|
||||
debugLogger,
|
||||
spawnAsync,
|
||||
LlmRole,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { Command } from '../../key/keyMatchers.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Spinner from 'ink-spinner';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core';
|
||||
import {
|
||||
debugLogger,
|
||||
spawnAsync,
|
||||
LlmRole,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { Command } from '../../key/keyMatchers.js';
|
||||
import { TextInput } from '../shared/TextInput.js';
|
||||
|
||||
@@ -8,7 +8,6 @@ import type React from 'react';
|
||||
import { useMemo, useCallback, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { RegistryExtension } from '../../../config/extensionRegistryClient.js';
|
||||
|
||||
import {
|
||||
SearchableList,
|
||||
type GenericListItem,
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import { MCPServerStatus } from '@google/gemini-cli-core';
|
||||
import { MCPServerStatus, type MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
import { MAX_MCP_RESOURCES_TO_SHOW } from '../../constants.js';
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AuthType,
|
||||
createContentGeneratorConfig,
|
||||
type ContentGenerator,
|
||||
validateBaseUrl,
|
||||
} from './contentGenerator.js';
|
||||
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
@@ -442,6 +443,116 @@ describe('createContentGenerator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass GOOGLE_GEMINI_BASE_URL as httpOptions.baseUrl for Gemini API', async () => {
|
||||
const mockConfig = {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
models: {},
|
||||
} as unknown as GoogleGenAI;
|
||||
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
|
||||
vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'https://my-gemini-proxy.example.com');
|
||||
|
||||
await createContentGenerator(
|
||||
{
|
||||
apiKey: 'test-api-key',
|
||||
authType: AuthType.USE_GEMINI,
|
||||
},
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
expect(GoogleGenAI).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
baseUrl: 'https://my-gemini-proxy.example.com',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass GOOGLE_VERTEX_BASE_URL as httpOptions.baseUrl for Vertex AI', async () => {
|
||||
const mockConfig = {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
models: {},
|
||||
} as unknown as GoogleGenAI;
|
||||
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
|
||||
vi.stubEnv('GOOGLE_VERTEX_BASE_URL', 'https://my-vertex-proxy.example.com');
|
||||
|
||||
await createContentGenerator(
|
||||
{
|
||||
apiKey: 'test-api-key',
|
||||
vertexai: true,
|
||||
authType: AuthType.USE_VERTEX_AI,
|
||||
},
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
expect(GoogleGenAI).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
baseUrl: 'https://my-vertex-proxy.example.com',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not include baseUrl in httpOptions when GOOGLE_GEMINI_BASE_URL is not set', async () => {
|
||||
const mockConfig = {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const mockGenerator = {
|
||||
models: {},
|
||||
} as unknown as GoogleGenAI;
|
||||
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
|
||||
|
||||
await createContentGenerator(
|
||||
{
|
||||
apiKey: 'test-api-key',
|
||||
authType: AuthType.USE_GEMINI,
|
||||
},
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
expect(GoogleGenAI).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
httpOptions: expect.objectContaining({
|
||||
baseUrl: expect.any(String),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an insecure GOOGLE_GEMINI_BASE_URL for non-local hosts', async () => {
|
||||
const mockConfig = {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'http://evil-proxy.example.com');
|
||||
|
||||
await expect(
|
||||
createContentGenerator(
|
||||
{
|
||||
apiKey: 'test-api-key',
|
||||
authType: AuthType.USE_GEMINI,
|
||||
},
|
||||
mockConfig,
|
||||
),
|
||||
).rejects.toThrow('Custom base URL must use HTTPS unless it is localhost.');
|
||||
});
|
||||
|
||||
it('should pass apiVersion for Vertex AI when GOOGLE_GENAI_API_VERSION is set', async () => {
|
||||
const mockConfig = {
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
@@ -560,3 +671,33 @@ describe('createContentGeneratorConfig', () => {
|
||||
expect(config.vertexai).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateBaseUrl', () => {
|
||||
it('should accept a valid HTTPS URL', () => {
|
||||
expect(() => validateBaseUrl('https://my-proxy.example.com')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept HTTP for localhost', () => {
|
||||
expect(() => validateBaseUrl('http://localhost:8080')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept HTTP for 127.0.0.1', () => {
|
||||
expect(() => validateBaseUrl('http://127.0.0.1:3000')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept HTTP for ::1', () => {
|
||||
expect(() => validateBaseUrl('http://[::1]:8080')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject HTTP for non-local hosts', () => {
|
||||
expect(() => validateBaseUrl('http://my-proxy.example.com')).toThrow(
|
||||
'Custom base URL must use HTTPS unless it is localhost.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an invalid URL', () => {
|
||||
expect(() => validateBaseUrl('not-a-url')).toThrow(
|
||||
'Invalid custom base URL: not-a-url',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,13 +225,25 @@ export async function createContentGenerator(
|
||||
'x-gemini-api-privileged-user-id': `${installationId}`,
|
||||
};
|
||||
}
|
||||
let baseUrl = config.baseUrl;
|
||||
if (!baseUrl) {
|
||||
const envBaseUrl = config.vertexai
|
||||
? process.env['GOOGLE_VERTEX_BASE_URL']
|
||||
: process.env['GOOGLE_GEMINI_BASE_URL'];
|
||||
if (envBaseUrl) {
|
||||
validateBaseUrl(envBaseUrl);
|
||||
baseUrl = envBaseUrl;
|
||||
}
|
||||
} else {
|
||||
validateBaseUrl(baseUrl);
|
||||
}
|
||||
const httpOptions: {
|
||||
baseUrl?: string;
|
||||
headers: Record<string, string>;
|
||||
} = { headers };
|
||||
|
||||
if (config.baseUrl) {
|
||||
httpOptions.baseUrl = config.baseUrl;
|
||||
if (baseUrl) {
|
||||
httpOptions.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
const googleGenAI = new GoogleGenAI({
|
||||
@@ -253,3 +265,17 @@ export async function createContentGenerator(
|
||||
|
||||
return generator;
|
||||
}
|
||||
|
||||
const LOCAL_HOSTNAMES = ['localhost', '127.0.0.1', '[::1]'];
|
||||
|
||||
export function validateBaseUrl(baseUrl: string): void {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(baseUrl);
|
||||
} catch {
|
||||
throw new Error(`Invalid custom base URL: ${baseUrl}`);
|
||||
}
|
||||
if (url.protocol !== 'https:' && !LOCAL_HOSTNAMES.includes(url.hostname)) {
|
||||
throw new Error('Custom base URL must use HTTPS unless it is localhost.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1380,11 +1380,11 @@ describe('GeminiChat', () => {
|
||||
}
|
||||
}).rejects.toThrow(InvalidStreamError);
|
||||
|
||||
// Should be called 2 times (initial + 1 retry)
|
||||
// Should be called 4 times (initial + 3 retries)
|
||||
expect(mockContentGenerator.generateContentStream).toHaveBeenCalledTimes(
|
||||
2,
|
||||
4,
|
||||
);
|
||||
expect(mockLogContentRetry).toHaveBeenCalledTimes(1);
|
||||
expect(mockLogContentRetry).toHaveBeenCalledTimes(3);
|
||||
expect(mockLogContentRetryFailure).toHaveBeenCalledTimes(1);
|
||||
|
||||
// History should still contain the user message.
|
||||
|
||||
@@ -79,17 +79,17 @@ export type StreamEvent =
|
||||
| { type: StreamEventType.AGENT_EXECUTION_BLOCKED; reason: string };
|
||||
|
||||
/**
|
||||
* Options for retrying due to invalid content from the model.
|
||||
* Options for retrying mid-stream errors (e.g. invalid content or API disconnects).
|
||||
*/
|
||||
interface ContentRetryOptions {
|
||||
interface MidStreamRetryOptions {
|
||||
/** Total number of attempts to make (1 initial + N retries). */
|
||||
maxAttempts: number;
|
||||
/** The base delay in milliseconds for linear backoff. */
|
||||
initialDelayMs: number;
|
||||
}
|
||||
|
||||
const INVALID_CONTENT_RETRY_OPTIONS: ContentRetryOptions = {
|
||||
maxAttempts: 2, // 1 initial call + 1 retry
|
||||
const MID_STREAM_RETRY_OPTIONS: MidStreamRetryOptions = {
|
||||
maxAttempts: 4, // 1 initial call + 3 retries mid-stream
|
||||
initialDelayMs: 500,
|
||||
};
|
||||
|
||||
@@ -350,7 +350,7 @@ export class GeminiChat {
|
||||
this: GeminiChat,
|
||||
): AsyncGenerator<StreamEvent, void, void> {
|
||||
try {
|
||||
const maxAttempts = INVALID_CONTENT_RETRY_OPTIONS.maxAttempts;
|
||||
const maxAttempts = this.config.getMaxAttempts();
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
let isConnectionPhase = true;
|
||||
@@ -402,21 +402,19 @@ export class GeminiChat {
|
||||
return; // Stop the generator
|
||||
}
|
||||
|
||||
if (isConnectionPhase) {
|
||||
// Connection phase errors have already been retried by retryWithBackoff.
|
||||
// If they bubble up here, they are exhausted or fatal.
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Check if the error is retryable (e.g., transient SSL errors
|
||||
// like ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC)
|
||||
// like ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC or ApiError)
|
||||
const isRetryable = isRetryableError(
|
||||
error,
|
||||
this.config.getRetryFetchErrors(),
|
||||
);
|
||||
|
||||
// For connection phase errors, only retryable errors should continue
|
||||
if (isConnectionPhase) {
|
||||
if (!isRetryable || signal.aborted) {
|
||||
throw error;
|
||||
}
|
||||
// Fall through to retry logic for retryable connection errors
|
||||
}
|
||||
|
||||
const isContentError = error instanceof InvalidStreamError;
|
||||
const errorType = isContentError
|
||||
? error.type
|
||||
@@ -426,9 +424,16 @@ export class GeminiChat {
|
||||
(isContentError && isGemini2Model(model)) ||
|
||||
(isRetryable && !signal.aborted)
|
||||
) {
|
||||
// Check if we have more attempts left.
|
||||
if (attempt < maxAttempts - 1) {
|
||||
const delayMs = INVALID_CONTENT_RETRY_OPTIONS.initialDelayMs;
|
||||
// The issue requests exactly 3 retries (4 attempts) for API errors during stream iteration.
|
||||
// Regardless of the global maxAttempts (e.g. 10), we only want to retry these mid-stream API errors
|
||||
// up to 3 times before finally throwing the error to the user.
|
||||
const maxMidStreamAttempts = MID_STREAM_RETRY_OPTIONS.maxAttempts;
|
||||
|
||||
if (
|
||||
attempt < maxAttempts - 1 &&
|
||||
attempt < maxMidStreamAttempts - 1
|
||||
) {
|
||||
const delayMs = MID_STREAM_RETRY_OPTIONS.initialDelayMs;
|
||||
|
||||
if (isContentError) {
|
||||
logContentRetry(
|
||||
@@ -449,7 +454,7 @@ export class GeminiChat {
|
||||
}
|
||||
coreEvents.emitRetryAttempt({
|
||||
attempt: attempt + 1,
|
||||
maxAttempts,
|
||||
maxAttempts: Math.min(maxAttempts, maxMidStreamAttempts),
|
||||
delayMs: delayMs * (attempt + 1),
|
||||
error: errorType,
|
||||
model,
|
||||
|
||||
@@ -292,6 +292,14 @@ describe('GeminiChat Network Retries', () => {
|
||||
(sslError as NodeJS.ErrnoException).code =
|
||||
'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC';
|
||||
|
||||
// Instead of outer loop, connection retries are handled by retryWithBackoff.
|
||||
// Simulate retryWithBackoff attempting it twice: first throws, second succeeds.
|
||||
mockRetryWithBackoff.mockImplementation(
|
||||
async (apiCall) =>
|
||||
// Execute the apiCall to trigger mockContentGenerator
|
||||
await apiCall(),
|
||||
);
|
||||
|
||||
vi.mocked(mockContentGenerator.generateContentStream)
|
||||
// First call: throw SSL error immediately (connection phase)
|
||||
.mockRejectedValueOnce(sslError)
|
||||
@@ -309,6 +317,15 @@ describe('GeminiChat Network Retries', () => {
|
||||
})(),
|
||||
);
|
||||
|
||||
// Because retryWithBackoff is mocked and we just want to test GeminiChat's integration,
|
||||
// we need to actually execute the real retryWithBackoff logic for this test to see it work.
|
||||
// So let's restore the real retryWithBackoff for this test.
|
||||
const { retryWithBackoff } =
|
||||
await vi.importActual<typeof import('../utils/retry.js')>(
|
||||
'../utils/retry.js',
|
||||
);
|
||||
mockRetryWithBackoff.mockImplementation(retryWithBackoff);
|
||||
|
||||
const stream = await chat.sendMessageStream(
|
||||
{ model: 'test-model' },
|
||||
'test message',
|
||||
@@ -322,10 +339,6 @@ describe('GeminiChat Network Retries', () => {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
// Should have retried and succeeded
|
||||
const retryEvent = events.find((e) => e.type === StreamEventType.RETRY);
|
||||
expect(retryEvent).toBeDefined();
|
||||
|
||||
const successChunk = events.find(
|
||||
(e) =>
|
||||
e.type === StreamEventType.CHUNK &&
|
||||
@@ -342,6 +355,12 @@ describe('GeminiChat Network Retries', () => {
|
||||
const connectionError = new Error('read ECONNRESET');
|
||||
(connectionError as NodeJS.ErrnoException).code = 'ECONNRESET';
|
||||
|
||||
const { retryWithBackoff } =
|
||||
await vi.importActual<typeof import('../utils/retry.js')>(
|
||||
'../utils/retry.js',
|
||||
);
|
||||
mockRetryWithBackoff.mockImplementation(retryWithBackoff);
|
||||
|
||||
vi.mocked(mockContentGenerator.generateContentStream)
|
||||
.mockRejectedValueOnce(connectionError)
|
||||
.mockImplementationOnce(async () =>
|
||||
@@ -372,9 +391,6 @@ describe('GeminiChat Network Retries', () => {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
const retryEvent = events.find((e) => e.type === StreamEventType.RETRY);
|
||||
expect(retryEvent).toBeDefined();
|
||||
|
||||
const successChunk = events.find(
|
||||
(e) =>
|
||||
e.type === StreamEventType.CHUNK &&
|
||||
@@ -382,6 +398,7 @@ describe('GeminiChat Network Retries', () => {
|
||||
'Success after connection retry',
|
||||
);
|
||||
expect(successChunk).toBeDefined();
|
||||
expect(mockContentGenerator.generateContentStream).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should NOT retry on non-retryable error during connection phase', async () => {
|
||||
|
||||
@@ -116,6 +116,7 @@ export function runSensitiveKeywordLinter() {
|
||||
console.log('\nRunning sensitive keyword linter...');
|
||||
const SENSITIVE_PATTERN = /gemini-\d+(\.\d+)?/g;
|
||||
const ALLOWED_KEYWORDS = new Set([
|
||||
'gemini-3.1',
|
||||
'gemini-3',
|
||||
'gemini-3.0',
|
||||
'gemini-2.5',
|
||||
|
||||
Reference in New Issue
Block a user