@google/gemini-cli-core 0.6.0-nightly.20250910.a31830a3 → 0.6.0-preview.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/README.md +12 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/src/code_assist/converter.d.ts +1 -0
- package/dist/src/code_assist/converter.js +1 -0
- package/dist/src/code_assist/converter.js.map +1 -1
- package/dist/src/code_assist/converter.test.js +10 -0
- package/dist/src/code_assist/converter.test.js.map +1 -1
- package/dist/src/code_assist/oauth-credential-storage.d.ts +5 -7
- package/dist/src/code_assist/oauth-credential-storage.js +5 -8
- package/dist/src/code_assist/oauth-credential-storage.js.map +1 -1
- package/dist/src/code_assist/oauth-credential-storage.test.js +35 -33
- package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -1
- package/dist/src/code_assist/oauth2.js +28 -2
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/oauth2.test.js +674 -536
- package/dist/src/code_assist/oauth2.test.js.map +1 -1
- package/dist/src/config/config.d.ts +32 -1
- package/dist/src/config/config.js +74 -17
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +104 -16
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/models.d.ts +15 -0
- package/dist/src/config/models.js +27 -0
- package/dist/src/config/models.js.map +1 -1
- package/dist/src/config/models.test.d.ts +6 -0
- package/dist/src/config/models.test.js +55 -0
- package/dist/src/config/models.test.js.map +1 -0
- package/dist/src/confirmation-bus/index.d.ts +7 -0
- package/dist/src/confirmation-bus/index.js +8 -0
- package/dist/src/confirmation-bus/index.js.map +1 -0
- package/dist/src/confirmation-bus/message-bus.d.ts +17 -0
- package/dist/src/confirmation-bus/message-bus.js +81 -0
- package/dist/src/confirmation-bus/message-bus.js.map +1 -0
- package/dist/src/confirmation-bus/message-bus.test.d.ts +6 -0
- package/dist/src/confirmation-bus/message-bus.test.js +164 -0
- package/dist/src/confirmation-bus/message-bus.test.js.map +1 -0
- package/dist/src/confirmation-bus/types.d.ts +38 -0
- package/dist/src/confirmation-bus/types.js +15 -0
- package/dist/src/confirmation-bus/types.js.map +1 -0
- package/dist/src/core/baseLlmClient.d.ts +1 -0
- package/dist/src/core/baseLlmClient.js +24 -0
- package/dist/src/core/baseLlmClient.js.map +1 -1
- package/dist/src/core/baseLlmClient.test.js +63 -0
- package/dist/src/core/baseLlmClient.test.js.map +1 -1
- package/dist/src/core/client.d.ts +5 -4
- package/dist/src/core/client.js +80 -140
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +247 -186
- package/dist/src/core/client.test.js.map +1 -1
- package/dist/src/core/contentGenerator.d.ts +0 -1
- package/dist/src/core/contentGenerator.js +0 -4
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/contentGenerator.test.js +0 -3
- package/dist/src/core/contentGenerator.test.js.map +1 -1
- package/dist/src/core/coreToolScheduler.d.ts +4 -3
- package/dist/src/core/coreToolScheduler.js +42 -5
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/coreToolScheduler.test.js +43 -0
- package/dist/src/core/coreToolScheduler.test.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +3 -30
- package/dist/src/core/geminiChat.js +32 -228
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/geminiChat.test.js +58 -489
- package/dist/src/core/geminiChat.test.js.map +1 -1
- package/dist/src/core/loggingContentGenerator.js +5 -5
- package/dist/src/core/loggingContentGenerator.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.test.js +49 -0
- package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
- package/dist/src/core/subagent.js +1 -1
- package/dist/src/core/subagent.js.map +1 -1
- package/dist/src/core/subagent.test.js +9 -8
- package/dist/src/core/subagent.test.js.map +1 -1
- package/dist/src/core/turn.d.ts +2 -1
- package/dist/src/core/turn.js +2 -2
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/core/turn.test.js +18 -18
- package/dist/src/core/turn.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/generated/git-commit.js.map +1 -1
- package/dist/src/ide/constants.d.ts +1 -0
- package/dist/src/ide/constants.js +1 -0
- package/dist/src/ide/constants.js.map +1 -1
- package/dist/src/ide/ide-client.d.ts +51 -13
- package/dist/src/ide/ide-client.js +184 -37
- package/dist/src/ide/ide-client.js.map +1 -1
- package/dist/src/ide/ide-client.test.js +93 -3
- package/dist/src/ide/ide-client.test.js.map +1 -1
- package/dist/src/ide/ide-installer.js +8 -2
- package/dist/src/ide/ide-installer.js.map +1 -1
- package/dist/src/ide/ide-installer.test.js +13 -2
- package/dist/src/ide/ide-installer.test.js.map +1 -1
- package/dist/src/ide/ideContext.d.ts +34 -113
- package/dist/src/ide/ideContext.js +20 -78
- package/dist/src/ide/ideContext.js.map +1 -1
- package/dist/src/ide/ideContext.test.js +37 -39
- package/dist/src/ide/ideContext.test.js.map +1 -1
- package/dist/src/ide/types.d.ts +141 -0
- package/dist/src/ide/types.js +73 -0
- package/dist/src/ide/types.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/oauth-token-storage.d.ts +2 -0
- package/dist/src/mcp/oauth-token-storage.js +25 -0
- package/dist/src/mcp/oauth-token-storage.js.map +1 -1
- package/dist/src/mcp/oauth-token-storage.test.js +251 -160
- package/dist/src/mcp/oauth-token-storage.test.js.map +1 -1
- package/dist/src/mcp/token-storage/index.d.ts +11 -0
- package/dist/src/mcp/token-storage/index.js +12 -0
- package/dist/src/mcp/token-storage/index.js.map +1 -0
- package/dist/src/output/json-formatter.d.ts +11 -0
- package/dist/src/output/json-formatter.js +30 -0
- package/dist/src/output/json-formatter.js.map +1 -0
- package/dist/src/output/json-formatter.test.d.ts +6 -0
- package/dist/src/output/json-formatter.test.js +266 -0
- package/dist/src/output/json-formatter.test.js.map +1 -0
- package/dist/src/output/types.d.ts +20 -0
- package/dist/src/output/types.js +11 -0
- package/dist/src/output/types.js.map +1 -0
- package/dist/src/policy/index.d.ts +7 -0
- package/dist/src/policy/index.js +8 -0
- package/dist/src/policy/index.js.map +1 -0
- package/dist/src/policy/policy-engine.d.ts +30 -0
- package/dist/src/policy/policy-engine.js +83 -0
- package/dist/src/policy/policy-engine.js.map +1 -0
- package/dist/src/policy/policy-engine.test.d.ts +6 -0
- package/dist/src/policy/policy-engine.test.js +470 -0
- package/dist/src/policy/policy-engine.test.js.map +1 -0
- package/dist/src/policy/stable-stringify.d.ts +58 -0
- package/dist/src/policy/stable-stringify.js +122 -0
- package/dist/src/policy/stable-stringify.js.map +1 -0
- package/dist/src/policy/types.d.ts +47 -0
- package/dist/src/policy/types.js +12 -0
- package/dist/src/policy/types.js.map +1 -0
- package/dist/src/routing/modelRouterService.d.ts +23 -0
- package/dist/src/routing/modelRouterService.js +70 -0
- package/dist/src/routing/modelRouterService.js.map +1 -0
- package/dist/src/routing/modelRouterService.test.d.ts +6 -0
- package/dist/src/routing/modelRouterService.test.js +98 -0
- package/dist/src/routing/modelRouterService.test.js.map +1 -0
- package/dist/src/routing/routingStrategy.d.ts +62 -0
- package/dist/src/routing/routingStrategy.js +7 -0
- package/dist/src/routing/routingStrategy.js.map +1 -0
- package/dist/src/routing/strategies/classifierStrategy.d.ts +12 -0
- package/dist/src/routing/strategies/classifierStrategy.js +173 -0
- package/dist/src/routing/strategies/classifierStrategy.js.map +1 -0
- package/dist/src/routing/strategies/classifierStrategy.test.d.ts +6 -0
- package/dist/src/routing/strategies/classifierStrategy.test.js +192 -0
- package/dist/src/routing/strategies/classifierStrategy.test.js.map +1 -0
- package/dist/src/routing/strategies/compositeStrategy.d.ts +26 -0
- package/dist/src/routing/strategies/compositeStrategy.js +67 -0
- package/dist/src/routing/strategies/compositeStrategy.js.map +1 -0
- package/dist/src/routing/strategies/compositeStrategy.test.d.ts +6 -0
- package/dist/src/routing/strategies/compositeStrategy.test.js +123 -0
- package/dist/src/routing/strategies/compositeStrategy.test.js.map +1 -0
- package/dist/src/routing/strategies/defaultStrategy.d.ts +12 -0
- package/dist/src/routing/strategies/defaultStrategy.js +20 -0
- package/dist/src/routing/strategies/defaultStrategy.js.map +1 -0
- package/dist/src/routing/strategies/defaultStrategy.test.d.ts +6 -0
- package/dist/src/routing/strategies/defaultStrategy.test.js +26 -0
- package/dist/src/routing/strategies/defaultStrategy.test.js.map +1 -0
- package/dist/src/routing/strategies/fallbackStrategy.d.ts +12 -0
- package/dist/src/routing/strategies/fallbackStrategy.js +25 -0
- package/dist/src/routing/strategies/fallbackStrategy.js.map +1 -0
- package/dist/src/routing/strategies/fallbackStrategy.test.d.ts +6 -0
- package/dist/src/routing/strategies/fallbackStrategy.test.js +55 -0
- package/dist/src/routing/strategies/fallbackStrategy.test.js.map +1 -0
- package/dist/src/routing/strategies/overrideStrategy.d.ts +15 -0
- package/dist/src/routing/strategies/overrideStrategy.js +28 -0
- package/dist/src/routing/strategies/overrideStrategy.js.map +1 -0
- package/dist/src/routing/strategies/overrideStrategy.test.d.ts +6 -0
- package/dist/src/routing/strategies/overrideStrategy.test.js +42 -0
- package/dist/src/routing/strategies/overrideStrategy.test.js.map +1 -0
- package/dist/src/services/chatRecordingService.d.ts +2 -1
- package/dist/src/services/chatRecordingService.js +3 -3
- package/dist/src/services/chatRecordingService.js.map +1 -1
- package/dist/src/services/chatRecordingService.test.js +8 -3
- package/dist/src/services/chatRecordingService.test.js.map +1 -1
- package/dist/src/services/fileDiscoveryService.d.ts +10 -0
- package/dist/src/services/fileDiscoveryService.js +31 -17
- package/dist/src/services/fileDiscoveryService.js.map +1 -1
- package/dist/src/services/gitService.js +9 -12
- package/dist/src/services/gitService.js.map +1 -1
- package/dist/src/services/gitService.test.js +10 -20
- package/dist/src/services/gitService.test.js.map +1 -1
- package/dist/src/services/loopDetectionService.d.ts +5 -0
- package/dist/src/services/loopDetectionService.js +36 -20
- package/dist/src/services/loopDetectionService.js.map +1 -1
- package/dist/src/services/loopDetectionService.test.js +41 -12
- package/dist/src/services/loopDetectionService.test.js.map +1 -1
- package/dist/src/services/shellExecutionService.d.ts +34 -2
- package/dist/src/services/shellExecutionService.js +177 -43
- package/dist/src/services/shellExecutionService.js.map +1 -1
- package/dist/src/services/shellExecutionService.test.js +153 -56
- package/dist/src/services/shellExecutionService.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +10 -2
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +85 -5
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +63 -5
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +12 -2
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +31 -2
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
- package/dist/src/telemetry/constants.d.ts +3 -0
- package/dist/src/telemetry/constants.js +3 -0
- package/dist/src/telemetry/constants.js.map +1 -1
- package/dist/src/telemetry/gcp-exporters.d.ts +34 -0
- package/dist/src/telemetry/gcp-exporters.js +117 -0
- package/dist/src/telemetry/gcp-exporters.js.map +1 -0
- package/dist/src/telemetry/gcp-exporters.test.d.ts +6 -0
- package/dist/src/telemetry/gcp-exporters.test.js +318 -0
- package/dist/src/telemetry/gcp-exporters.test.js.map +1 -0
- package/dist/src/telemetry/index.d.ts +3 -2
- package/dist/src/telemetry/index.js +3 -2
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/loggers.d.ts +4 -1
- package/dist/src/telemetry/loggers.js +42 -7
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/loggers.test.js +84 -36
- package/dist/src/telemetry/loggers.test.js.map +1 -1
- package/dist/src/telemetry/metrics.d.ts +3 -1
- package/dist/src/telemetry/metrics.js +32 -3
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/metrics.test.js +42 -0
- package/dist/src/telemetry/metrics.test.js.map +1 -1
- package/dist/src/telemetry/sdk.js +16 -1
- package/dist/src/telemetry/sdk.js.map +1 -1
- package/dist/src/telemetry/sdk.test.js +95 -0
- package/dist/src/telemetry/sdk.test.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +47 -3
- package/dist/src/telemetry/types.js +67 -3
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/tools/edit.js +6 -5
- package/dist/src/tools/edit.js.map +1 -1
- package/dist/src/tools/edit.test.js +79 -9
- package/dist/src/tools/edit.test.js.map +1 -1
- package/dist/src/tools/glob.d.ts +5 -1
- package/dist/src/tools/glob.js +24 -17
- package/dist/src/tools/glob.js.map +1 -1
- package/dist/src/tools/glob.test.js +51 -0
- package/dist/src/tools/glob.test.js.map +1 -1
- package/dist/src/tools/ls.js +19 -32
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/ls.test.js +140 -280
- package/dist/src/tools/ls.test.js.map +1 -1
- package/dist/src/tools/read-many-files.d.ts +1 -1
- package/dist/src/tools/read-many-files.js +17 -49
- package/dist/src/tools/read-many-files.js.map +1 -1
- package/dist/src/tools/ripGrep.d.ts +4 -0
- package/dist/src/tools/ripGrep.js +11 -1
- package/dist/src/tools/ripGrep.js.map +1 -1
- package/dist/src/tools/ripGrep.test.js +51 -1
- package/dist/src/tools/ripGrep.test.js.map +1 -1
- package/dist/src/tools/shell.d.ts +12 -2
- package/dist/src/tools/shell.js +20 -27
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/shell.test.js +33 -68
- package/dist/src/tools/shell.test.js.map +1 -1
- package/dist/src/tools/smart-edit.d.ts +0 -1
- package/dist/src/tools/smart-edit.js +5 -18
- package/dist/src/tools/smart-edit.js.map +1 -1
- package/dist/src/tools/smart-edit.test.js +18 -9
- package/dist/src/tools/smart-edit.test.js.map +1 -1
- package/dist/src/tools/tools.d.ts +7 -5
- package/dist/src/tools/tools.js +2 -2
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/write-file.js +4 -5
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/tools/write-file.test.js +94 -10
- package/dist/src/tools/write-file.test.js.map +1 -1
- package/dist/src/utils/bfsFileSearch.js +11 -5
- package/dist/src/utils/bfsFileSearch.js.map +1 -1
- package/dist/src/utils/editCorrector.d.ts +7 -6
- package/dist/src/utils/editCorrector.js +61 -18
- package/dist/src/utils/editCorrector.js.map +1 -1
- package/dist/src/utils/editCorrector.test.js +30 -79
- package/dist/src/utils/editCorrector.test.js.map +1 -1
- package/dist/src/utils/editor.js +31 -44
- package/dist/src/utils/editor.js.map +1 -1
- package/dist/src/utils/editor.test.js +61 -75
- package/dist/src/utils/editor.test.js.map +1 -1
- package/dist/src/utils/errorParsing.js +2 -2
- package/dist/src/utils/errorParsing.js.map +1 -1
- package/dist/src/utils/errorParsing.test.js +7 -7
- package/dist/src/utils/errorParsing.test.js.map +1 -1
- package/dist/src/utils/errors.d.ts +6 -0
- package/dist/src/utils/errors.js +10 -0
- package/dist/src/utils/errors.js.map +1 -1
- package/dist/src/utils/fileUtils.test.js +17 -8
- package/dist/src/utils/fileUtils.test.js.map +1 -1
- package/dist/src/utils/geminiIgnoreParser.d.ts +18 -0
- package/dist/src/utils/geminiIgnoreParser.js +61 -0
- package/dist/src/utils/geminiIgnoreParser.js.map +1 -0
- package/dist/src/utils/geminiIgnoreParser.test.d.ts +6 -0
- package/dist/src/utils/geminiIgnoreParser.test.js +50 -0
- package/dist/src/utils/geminiIgnoreParser.test.js.map +1 -0
- package/dist/src/utils/gitIgnoreParser.d.ts +3 -9
- package/dist/src/utils/gitIgnoreParser.js +60 -69
- package/dist/src/utils/gitIgnoreParser.js.map +1 -1
- package/dist/src/utils/gitIgnoreParser.test.js +18 -53
- package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.test.js +12 -6
- package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
- package/dist/src/utils/nextSpeakerChecker.d.ts +2 -2
- package/dist/src/utils/nextSpeakerChecker.js +8 -2
- package/dist/src/utils/nextSpeakerChecker.js.map +1 -1
- package/dist/src/utils/nextSpeakerChecker.test.js +40 -33
- package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
- package/dist/src/utils/shell-utils.d.ts +5 -0
- package/dist/src/utils/shell-utils.js +23 -0
- package/dist/src/utils/shell-utils.js.map +1 -1
- package/dist/src/utils/terminalSerializer.d.ts +28 -0
- package/dist/src/utils/terminalSerializer.js +432 -0
- package/dist/src/utils/terminalSerializer.js.map +1 -0
- package/dist/src/utils/terminalSerializer.test.d.ts +6 -0
- package/dist/src/utils/terminalSerializer.test.js +176 -0
- package/dist/src/utils/terminalSerializer.test.js.map +1 -0
- package/dist/src/utils/textUtils.d.ts +5 -0
- package/dist/src/utils/textUtils.js +14 -0
- package/dist/src/utils/textUtils.js.map +1 -1
- package/dist/src/utils/textUtils.test.d.ts +6 -0
- package/dist/src/utils/textUtils.test.js +59 -0
- package/dist/src/utils/textUtils.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -1
- package/dist/src/utils/ide-trust.d.ts +0 -10
- package/dist/src/utils/ide-trust.js +0 -14
- package/dist/src/utils/ide-trust.js.map +0 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
|
|
7
|
+
import { createUserContent } from '@google/genai';
|
|
7
8
|
import { findIndexAfterFraction, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
|
|
8
9
|
import { AuthType, } from './contentGenerator.js';
|
|
9
10
|
import {} from './geminiChat.js';
|
|
@@ -13,7 +14,7 @@ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
|
|
13
14
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
|
14
15
|
import { setSimulate429 } from '../utils/testUtils.js';
|
|
15
16
|
import { tokenLimit } from './tokenLimits.js';
|
|
16
|
-
import {
|
|
17
|
+
import { ideContextStore } from '../ide/ideContext.js';
|
|
17
18
|
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
|
18
19
|
// Mock fs module to prevent actual file system operations during tests
|
|
19
20
|
const mockFileSystem = new Map();
|
|
@@ -108,22 +109,22 @@ describe('findIndexAfterFraction', () => {
|
|
|
108
109
|
// 0: 66
|
|
109
110
|
// 1: 66 + 68 = 134
|
|
110
111
|
// 2: 134 + 66 = 200
|
|
111
|
-
// 200 >= 166.5, so index is
|
|
112
|
-
expect(findIndexAfterFraction(history, 0.5)).toBe(
|
|
112
|
+
// 200 >= 166.5, so index is 3
|
|
113
|
+
expect(findIndexAfterFraction(history, 0.5)).toBe(3);
|
|
113
114
|
});
|
|
114
115
|
it('should handle a fraction that results in the last index', () => {
|
|
115
116
|
// 333 * 0.9 = 299.7
|
|
116
117
|
// ...
|
|
117
118
|
// 3: 200 + 68 = 268
|
|
118
119
|
// 4: 268 + 65 = 333
|
|
119
|
-
// 333 >= 299.7, so index is
|
|
120
|
-
expect(findIndexAfterFraction(history, 0.9)).toBe(
|
|
120
|
+
// 333 >= 299.7, so index is 5
|
|
121
|
+
expect(findIndexAfterFraction(history, 0.9)).toBe(5);
|
|
121
122
|
});
|
|
122
123
|
it('should handle an empty history', () => {
|
|
123
124
|
expect(findIndexAfterFraction([], 0.5)).toBe(0);
|
|
124
125
|
});
|
|
125
126
|
it('should handle a history with only one item', () => {
|
|
126
|
-
expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(
|
|
127
|
+
expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(1);
|
|
127
128
|
});
|
|
128
129
|
it('should handle history with weird parts', () => {
|
|
129
130
|
const historyWithEmptyParts = [
|
|
@@ -131,7 +132,7 @@ describe('findIndexAfterFraction', () => {
|
|
|
131
132
|
{ role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
|
|
132
133
|
{ role: 'user', parts: [{ text: 'Message 2' }] },
|
|
133
134
|
];
|
|
134
|
-
expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(
|
|
135
|
+
expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(2);
|
|
135
136
|
});
|
|
136
137
|
});
|
|
137
138
|
describe('isThinkingSupported', () => {
|
|
@@ -176,8 +177,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
176
177
|
mockContentGenerator = {
|
|
177
178
|
generateContent: mockGenerateContentFn,
|
|
178
179
|
generateContentStream: vi.fn(),
|
|
179
|
-
countTokens: vi.fn(),
|
|
180
|
-
embedContent: vi.fn(),
|
|
180
|
+
countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
|
|
181
181
|
batchEmbedContents: vi.fn(),
|
|
182
182
|
};
|
|
183
183
|
// Because the GeminiClient constructor kicks off an async process (startChat)
|
|
@@ -189,7 +189,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
189
189
|
};
|
|
190
190
|
const fileService = new FileDiscoveryService('/test/dir');
|
|
191
191
|
const contentGeneratorConfig = {
|
|
192
|
-
model: 'test-model',
|
|
193
192
|
apiKey: 'test-key',
|
|
194
193
|
vertexai: false,
|
|
195
194
|
authType: AuthType.USE_GEMINI,
|
|
@@ -222,16 +221,26 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
222
221
|
getDirectories: vi.fn().mockReturnValue(['/test/dir']),
|
|
223
222
|
}),
|
|
224
223
|
getGeminiClient: vi.fn(),
|
|
224
|
+
getModelRouterService: vi.fn().mockReturnValue({
|
|
225
|
+
route: vi.fn().mockResolvedValue({ model: 'default-routed-model' }),
|
|
226
|
+
}),
|
|
225
227
|
isInFallbackMode: vi.fn().mockReturnValue(false),
|
|
226
228
|
setFallbackMode: vi.fn(),
|
|
227
229
|
getChatCompression: vi.fn().mockReturnValue(undefined),
|
|
228
230
|
getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
|
|
229
231
|
getUseSmartEdit: vi.fn().mockReturnValue(false),
|
|
232
|
+
getUseModelRouter: vi.fn().mockReturnValue(false),
|
|
230
233
|
getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
|
|
231
234
|
storage: {
|
|
232
235
|
getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
|
|
233
236
|
},
|
|
234
237
|
getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
|
|
238
|
+
getBaseLlmClient: vi.fn().mockReturnValue({
|
|
239
|
+
generateJson: vi.fn().mockResolvedValue({
|
|
240
|
+
next_speaker: 'user',
|
|
241
|
+
reasoning: 'test',
|
|
242
|
+
}),
|
|
243
|
+
}),
|
|
235
244
|
};
|
|
236
245
|
client = new GeminiClient(mockConfig);
|
|
237
246
|
await client.initialize();
|
|
@@ -240,129 +249,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
240
249
|
afterEach(() => {
|
|
241
250
|
vi.restoreAllMocks();
|
|
242
251
|
});
|
|
243
|
-
describe('generateEmbedding', () => {
|
|
244
|
-
const texts = ['hello world', 'goodbye world'];
|
|
245
|
-
const testEmbeddingModel = 'test-embedding-model';
|
|
246
|
-
it('should call embedContent with correct parameters and return embeddings', async () => {
|
|
247
|
-
const mockEmbeddings = [
|
|
248
|
-
[0.1, 0.2, 0.3],
|
|
249
|
-
[0.4, 0.5, 0.6],
|
|
250
|
-
];
|
|
251
|
-
vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
|
|
252
|
-
embeddings: [
|
|
253
|
-
{ values: mockEmbeddings[0] },
|
|
254
|
-
{ values: mockEmbeddings[1] },
|
|
255
|
-
],
|
|
256
|
-
});
|
|
257
|
-
const result = await client.generateEmbedding(texts);
|
|
258
|
-
expect(mockContentGenerator.embedContent).toHaveBeenCalledTimes(1);
|
|
259
|
-
expect(mockContentGenerator.embedContent).toHaveBeenCalledWith({
|
|
260
|
-
model: testEmbeddingModel,
|
|
261
|
-
contents: texts,
|
|
262
|
-
});
|
|
263
|
-
expect(result).toEqual(mockEmbeddings);
|
|
264
|
-
});
|
|
265
|
-
it('should return an empty array if an empty array is passed', async () => {
|
|
266
|
-
const result = await client.generateEmbedding([]);
|
|
267
|
-
expect(result).toEqual([]);
|
|
268
|
-
expect(mockContentGenerator.embedContent).not.toHaveBeenCalled();
|
|
269
|
-
});
|
|
270
|
-
it('should throw an error if API response has no embeddings array', async () => {
|
|
271
|
-
vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({});
|
|
272
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
|
|
273
|
-
});
|
|
274
|
-
it('should throw an error if API response has an empty embeddings array', async () => {
|
|
275
|
-
vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
|
|
276
|
-
embeddings: [],
|
|
277
|
-
});
|
|
278
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
|
|
279
|
-
});
|
|
280
|
-
it('should throw an error if API returns a mismatched number of embeddings', async () => {
|
|
281
|
-
vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
|
|
282
|
-
embeddings: [{ values: [1, 2, 3] }], // Only one for two texts
|
|
283
|
-
});
|
|
284
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned a mismatched number of embeddings. Expected 2, got 1.');
|
|
285
|
-
});
|
|
286
|
-
it('should throw an error if any embedding has nullish values', async () => {
|
|
287
|
-
vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
|
|
288
|
-
embeddings: [{ values: [1, 2, 3] }, { values: undefined }], // Second one is bad
|
|
289
|
-
});
|
|
290
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 1: "goodbye world"');
|
|
291
|
-
});
|
|
292
|
-
it('should throw an error if any embedding has an empty values array', async () => {
|
|
293
|
-
vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
|
|
294
|
-
embeddings: [{ values: [] }, { values: [1, 2, 3] }], // First one is bad
|
|
295
|
-
});
|
|
296
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 0: "hello world"');
|
|
297
|
-
});
|
|
298
|
-
it('should propagate errors from the API call', async () => {
|
|
299
|
-
vi.mocked(mockContentGenerator.embedContent).mockRejectedValue(new Error('API Failure'));
|
|
300
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('API Failure');
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
describe('generateJson', () => {
|
|
304
|
-
it('should call generateContent with the correct parameters', async () => {
|
|
305
|
-
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
|
306
|
-
const schema = { type: 'string' };
|
|
307
|
-
const abortSignal = new AbortController().signal;
|
|
308
|
-
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
309
|
-
totalTokens: 1,
|
|
310
|
-
});
|
|
311
|
-
await client.generateJson(contents, schema, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
|
|
312
|
-
expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
|
|
313
|
-
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
314
|
-
config: {
|
|
315
|
-
abortSignal,
|
|
316
|
-
systemInstruction: getCoreSystemPrompt(''),
|
|
317
|
-
temperature: 0,
|
|
318
|
-
topP: 1,
|
|
319
|
-
responseJsonSchema: schema,
|
|
320
|
-
responseMimeType: 'application/json',
|
|
321
|
-
},
|
|
322
|
-
contents,
|
|
323
|
-
}, 'test-session-id');
|
|
324
|
-
});
|
|
325
|
-
it('should allow overriding model and config', async () => {
|
|
326
|
-
const contents = [
|
|
327
|
-
{ role: 'user', parts: [{ text: 'hello' }] },
|
|
328
|
-
];
|
|
329
|
-
const schema = { type: 'string' };
|
|
330
|
-
const abortSignal = new AbortController().signal;
|
|
331
|
-
const customModel = 'custom-json-model';
|
|
332
|
-
const customConfig = { temperature: 0.9, topK: 20 };
|
|
333
|
-
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
334
|
-
totalTokens: 1,
|
|
335
|
-
});
|
|
336
|
-
await client.generateJson(contents, schema, abortSignal, customModel, customConfig);
|
|
337
|
-
expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
|
|
338
|
-
model: customModel,
|
|
339
|
-
config: {
|
|
340
|
-
abortSignal,
|
|
341
|
-
systemInstruction: getCoreSystemPrompt(''),
|
|
342
|
-
temperature: 0.9,
|
|
343
|
-
topP: 1, // from default
|
|
344
|
-
topK: 20,
|
|
345
|
-
responseJsonSchema: schema,
|
|
346
|
-
responseMimeType: 'application/json',
|
|
347
|
-
},
|
|
348
|
-
contents,
|
|
349
|
-
}, 'test-session-id');
|
|
350
|
-
});
|
|
351
|
-
it('should use the Flash model when fallback mode is active', async () => {
|
|
352
|
-
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
|
353
|
-
const schema = { type: 'string' };
|
|
354
|
-
const abortSignal = new AbortController().signal;
|
|
355
|
-
const requestedModel = 'gemini-2.5-pro'; // A non-flash model
|
|
356
|
-
// Mock config to be in fallback mode
|
|
357
|
-
// We access the mock via the client instance which holds the mocked config
|
|
358
|
-
vi.spyOn(client['config'], 'isInFallbackMode').mockReturnValue(true);
|
|
359
|
-
await client.generateJson(contents, schema, abortSignal, requestedModel);
|
|
360
|
-
// Assert that the Flash model was used, not the requested model
|
|
361
|
-
expect(mockContentGenerator.generateContent).toHaveBeenCalledWith(expect.objectContaining({
|
|
362
|
-
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
363
|
-
}), 'test-session-id');
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
252
|
describe('addHistory', () => {
|
|
367
253
|
it('should call chat.addHistory with the provided content', async () => {
|
|
368
254
|
const mockChat = {
|
|
@@ -400,7 +286,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
400
286
|
});
|
|
401
287
|
});
|
|
402
288
|
describe('tryCompressChat', () => {
|
|
403
|
-
const mockSendMessage = vi.fn();
|
|
404
289
|
const mockGetHistory = vi.fn();
|
|
405
290
|
beforeEach(() => {
|
|
406
291
|
vi.mock('./tokenLimits', () => ({
|
|
@@ -410,7 +295,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
410
295
|
getHistory: mockGetHistory,
|
|
411
296
|
addHistory: vi.fn(),
|
|
412
297
|
setHistory: vi.fn(),
|
|
413
|
-
sendMessage: mockSendMessage,
|
|
414
298
|
};
|
|
415
299
|
});
|
|
416
300
|
function setup({ chatHistory = [
|
|
@@ -420,7 +304,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
420
304
|
const mockChat = {
|
|
421
305
|
getHistory: vi.fn().mockReturnValue(chatHistory),
|
|
422
306
|
setHistory: vi.fn(),
|
|
423
|
-
sendMessage: vi.fn().mockResolvedValue({ text: 'Summary' }),
|
|
424
307
|
};
|
|
425
308
|
vi.mocked(mockContentGenerator.countTokens)
|
|
426
309
|
.mockResolvedValueOnce({ totalTokens: 1000 })
|
|
@@ -435,8 +318,12 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
435
318
|
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
436
319
|
totalTokens: 1000,
|
|
437
320
|
});
|
|
438
|
-
await client.tryCompressChat('prompt-id-4'
|
|
439
|
-
|
|
321
|
+
await client.tryCompressChat('prompt-id-4', false, [
|
|
322
|
+
{ text: 'request' },
|
|
323
|
+
]); // Fails
|
|
324
|
+
const result = await client.tryCompressChat('prompt-id-4', true, [
|
|
325
|
+
{ text: 'request' },
|
|
326
|
+
]);
|
|
440
327
|
expect(result).toEqual({
|
|
441
328
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
442
329
|
newTokenCount: 1000,
|
|
@@ -448,7 +335,9 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
448
335
|
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
449
336
|
totalTokens: 1000,
|
|
450
337
|
});
|
|
451
|
-
const result = await client.tryCompressChat('prompt-id-4',
|
|
338
|
+
const result = await client.tryCompressChat('prompt-id-4', false, [
|
|
339
|
+
{ text: 'request' },
|
|
340
|
+
]);
|
|
452
341
|
expect(result).toEqual({
|
|
453
342
|
compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
|
454
343
|
newTokenCount: 5000,
|
|
@@ -457,7 +346,9 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
457
346
|
});
|
|
458
347
|
it('does not manipulate the source chat', async () => {
|
|
459
348
|
const { client, mockChat } = setup();
|
|
460
|
-
await client.tryCompressChat('prompt-id-4',
|
|
349
|
+
await client.tryCompressChat('prompt-id-4', false, [
|
|
350
|
+
{ text: 'request' },
|
|
351
|
+
]);
|
|
461
352
|
expect(client['chat']).toBe(mockChat); // a new chat session was not created
|
|
462
353
|
});
|
|
463
354
|
it('restores the history back to the original', async () => {
|
|
@@ -473,14 +364,18 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
473
364
|
const { client } = setup({
|
|
474
365
|
chatHistory: originalHistory,
|
|
475
366
|
});
|
|
476
|
-
const { compressionStatus } = await client.tryCompressChat('prompt-id-4');
|
|
367
|
+
const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false, [{ text: 'what is your wisdom?' }]);
|
|
477
368
|
expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
|
|
478
369
|
expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
|
|
479
370
|
});
|
|
480
371
|
it('will not attempt to compress context after a failure', async () => {
|
|
481
372
|
const { client } = setup();
|
|
482
|
-
await client.tryCompressChat('prompt-id-4'
|
|
483
|
-
|
|
373
|
+
await client.tryCompressChat('prompt-id-4', false, [
|
|
374
|
+
{ text: 'request' },
|
|
375
|
+
]);
|
|
376
|
+
const result = await client.tryCompressChat('prompt-id-5', false, [
|
|
377
|
+
{ text: 'request' },
|
|
378
|
+
]);
|
|
484
379
|
// it counts tokens for {original, compressed} and then never again
|
|
485
380
|
expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
|
|
486
381
|
expect(result).toEqual({
|
|
@@ -500,7 +395,9 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
500
395
|
totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
|
|
501
396
|
});
|
|
502
397
|
const initialChat = client.getChat();
|
|
503
|
-
const result = await client.tryCompressChat('prompt-id-2'
|
|
398
|
+
const result = await client.tryCompressChat('prompt-id-2', false, [
|
|
399
|
+
{ text: '...history...' },
|
|
400
|
+
]);
|
|
504
401
|
const newChat = client.getChat();
|
|
505
402
|
expect(tokenLimit).toHaveBeenCalled();
|
|
506
403
|
expect(result).toEqual({
|
|
@@ -527,11 +424,19 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
527
424
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
|
|
528
425
|
.mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
|
|
529
426
|
// Mock the summary response from the chat
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
427
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
428
|
+
candidates: [
|
|
429
|
+
{
|
|
430
|
+
content: {
|
|
431
|
+
role: 'model',
|
|
432
|
+
parts: [{ text: 'This is a summary.' }],
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
],
|
|
533
436
|
});
|
|
534
|
-
await client.tryCompressChat('prompt-id-3'
|
|
437
|
+
await client.tryCompressChat('prompt-id-3', false, [
|
|
438
|
+
{ text: '...history...' },
|
|
439
|
+
]);
|
|
535
440
|
expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
|
|
536
441
|
tokens_before: originalTokenCount,
|
|
537
442
|
tokens_after: newTokenCount,
|
|
@@ -553,15 +458,23 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
553
458
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
|
|
554
459
|
.mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
|
|
555
460
|
// Mock the summary response from the chat
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
461
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
462
|
+
candidates: [
|
|
463
|
+
{
|
|
464
|
+
content: {
|
|
465
|
+
role: 'model',
|
|
466
|
+
parts: [{ text: 'This is a summary.' }],
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
],
|
|
559
470
|
});
|
|
560
471
|
const initialChat = client.getChat();
|
|
561
|
-
const result = await client.tryCompressChat('prompt-id-3'
|
|
472
|
+
const result = await client.tryCompressChat('prompt-id-3', false, [
|
|
473
|
+
{ text: '...history...' },
|
|
474
|
+
]);
|
|
562
475
|
const newChat = client.getChat();
|
|
563
476
|
expect(tokenLimit).toHaveBeenCalled();
|
|
564
|
-
expect(
|
|
477
|
+
expect(mockGenerateContentFn).toHaveBeenCalled();
|
|
565
478
|
// Assert that summarization happened and returned the correct stats
|
|
566
479
|
expect(result).toEqual({
|
|
567
480
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
@@ -598,15 +511,23 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
598
511
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
|
|
599
512
|
.mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
|
|
600
513
|
// Mock the summary response from the chat
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
514
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
515
|
+
candidates: [
|
|
516
|
+
{
|
|
517
|
+
content: {
|
|
518
|
+
role: 'model',
|
|
519
|
+
parts: [{ text: 'This is a summary.' }],
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
],
|
|
604
523
|
});
|
|
605
524
|
const initialChat = client.getChat();
|
|
606
|
-
const result = await client.tryCompressChat('prompt-id-3'
|
|
525
|
+
const result = await client.tryCompressChat('prompt-id-3', false, [
|
|
526
|
+
{ text: '...history...' },
|
|
527
|
+
]);
|
|
607
528
|
const newChat = client.getChat();
|
|
608
529
|
expect(tokenLimit).toHaveBeenCalled();
|
|
609
|
-
expect(
|
|
530
|
+
expect(mockGenerateContentFn).toHaveBeenCalled();
|
|
610
531
|
// Assert that summarization happened and returned the correct stats
|
|
611
532
|
expect(result).toEqual({
|
|
612
533
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
@@ -620,7 +541,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
620
541
|
// 3. compressed summary message
|
|
621
542
|
// 4. standard canned user summary message
|
|
622
543
|
// 5. The last user message (not the last 3 because that would start with a function response)
|
|
623
|
-
expect(newChat.getHistory().length).toEqual(
|
|
544
|
+
expect(newChat.getHistory().length).toEqual(6);
|
|
624
545
|
});
|
|
625
546
|
it('should always trigger summarization when force is true, regardless of token count', async () => {
|
|
626
547
|
mockGetHistory.mockReturnValue([
|
|
@@ -632,14 +553,22 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
632
553
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount })
|
|
633
554
|
.mockResolvedValueOnce({ totalTokens: newTokenCount });
|
|
634
555
|
// Mock the summary response from the chat
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
556
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
557
|
+
candidates: [
|
|
558
|
+
{
|
|
559
|
+
content: {
|
|
560
|
+
role: 'model',
|
|
561
|
+
parts: [{ text: 'This is a summary.' }],
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
],
|
|
638
565
|
});
|
|
639
566
|
const initialChat = client.getChat();
|
|
640
|
-
const result = await client.tryCompressChat('prompt-id-1',
|
|
567
|
+
const result = await client.tryCompressChat('prompt-id-1', false, [
|
|
568
|
+
{ text: '...history...' },
|
|
569
|
+
]); // force = true
|
|
641
570
|
const newChat = client.getChat();
|
|
642
|
-
expect(
|
|
571
|
+
expect(mockGenerateContentFn).toHaveBeenCalled();
|
|
643
572
|
expect(result).toEqual({
|
|
644
573
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
645
574
|
originalTokenCount,
|
|
@@ -665,17 +594,18 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
665
594
|
{ role: 'model', parts: [{ text: 'Long response' }] },
|
|
666
595
|
];
|
|
667
596
|
const mockChat = {
|
|
668
|
-
getHistory: vi.fn().
|
|
597
|
+
getHistory: vi.fn().mockImplementation(() => [...mockChatHistory]),
|
|
669
598
|
setHistory: vi.fn(),
|
|
670
599
|
sendMessage: mockSendMessage,
|
|
671
600
|
};
|
|
672
601
|
client['chat'] = mockChat;
|
|
673
602
|
client['startChat'] = vi.fn().mockResolvedValue(mockChat);
|
|
674
|
-
const
|
|
603
|
+
const request = [{ text: 'Long conversation' }];
|
|
604
|
+
const result = await client.tryCompressChat('prompt-id-4', false, request);
|
|
675
605
|
expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
|
|
676
606
|
expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(1, {
|
|
677
607
|
model: firstCurrentModel,
|
|
678
|
-
contents: mockChatHistory,
|
|
608
|
+
contents: [...mockChatHistory, createUserContent(request)],
|
|
679
609
|
});
|
|
680
610
|
expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(2, {
|
|
681
611
|
model: secondCurrentModel,
|
|
@@ -740,7 +670,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
740
670
|
});
|
|
741
671
|
it('should include editor context when ideMode is enabled', async () => {
|
|
742
672
|
// Arrange
|
|
743
|
-
vi.mocked(
|
|
673
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
744
674
|
workspaceState: {
|
|
745
675
|
openFiles: [
|
|
746
676
|
{
|
|
@@ -762,6 +692,11 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
762
692
|
},
|
|
763
693
|
});
|
|
764
694
|
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
|
|
695
|
+
vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
|
|
696
|
+
originalTokenCount: 0,
|
|
697
|
+
newTokenCount: 0,
|
|
698
|
+
compressionStatus: CompressionStatus.COMPRESSED,
|
|
699
|
+
});
|
|
765
700
|
mockTurnRunFn.mockReturnValue((async function* () {
|
|
766
701
|
yield { type: 'content', value: 'Hello' };
|
|
767
702
|
})());
|
|
@@ -777,7 +712,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
777
712
|
// consume stream
|
|
778
713
|
}
|
|
779
714
|
// Assert
|
|
780
|
-
expect(
|
|
715
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
781
716
|
const expectedContext = `
|
|
782
717
|
Here is the user's editor context as a JSON object. This is for your information only.
|
|
783
718
|
\`\`\`json
|
|
@@ -802,7 +737,7 @@ ${JSON.stringify({
|
|
|
802
737
|
});
|
|
803
738
|
it('should not add context if ideMode is enabled but no open files', async () => {
|
|
804
739
|
// Arrange
|
|
805
|
-
vi.mocked(
|
|
740
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
806
741
|
workspaceState: {
|
|
807
742
|
openFiles: [],
|
|
808
743
|
},
|
|
@@ -824,12 +759,16 @@ ${JSON.stringify({
|
|
|
824
759
|
// consume stream
|
|
825
760
|
}
|
|
826
761
|
// Assert
|
|
827
|
-
expect(
|
|
828
|
-
|
|
762
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
763
|
+
// The `turn.run` method is now called with the model name as the first
|
|
764
|
+
// argument. We use `expect.any(String)` because this test is
|
|
765
|
+
// concerned with the IDE context logic, not the model routing,
|
|
766
|
+
// which is tested in its own dedicated suite.
|
|
767
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith(expect.any(String), initialRequest, expect.any(Object));
|
|
829
768
|
});
|
|
830
769
|
it('should add context if ideMode is enabled and there is one active file', async () => {
|
|
831
770
|
// Arrange
|
|
832
|
-
vi.mocked(
|
|
771
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
833
772
|
workspaceState: {
|
|
834
773
|
openFiles: [
|
|
835
774
|
{
|
|
@@ -843,6 +782,11 @@ ${JSON.stringify({
|
|
|
843
782
|
},
|
|
844
783
|
});
|
|
845
784
|
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
|
|
785
|
+
vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
|
|
786
|
+
originalTokenCount: 0,
|
|
787
|
+
newTokenCount: 0,
|
|
788
|
+
compressionStatus: CompressionStatus.COMPRESSED,
|
|
789
|
+
});
|
|
846
790
|
const mockStream = (async function* () {
|
|
847
791
|
yield { type: 'content', value: 'Hello' };
|
|
848
792
|
})();
|
|
@@ -859,7 +803,7 @@ ${JSON.stringify({
|
|
|
859
803
|
// consume stream
|
|
860
804
|
}
|
|
861
805
|
// Assert
|
|
862
|
-
expect(
|
|
806
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
863
807
|
const expectedContext = `
|
|
864
808
|
Here is the user's editor context as a JSON object. This is for your information only.
|
|
865
809
|
\`\`\`json
|
|
@@ -883,7 +827,7 @@ ${JSON.stringify({
|
|
|
883
827
|
});
|
|
884
828
|
it('should add context if ideMode is enabled and there are open files but no active file', async () => {
|
|
885
829
|
// Arrange
|
|
886
|
-
vi.mocked(
|
|
830
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
887
831
|
workspaceState: {
|
|
888
832
|
openFiles: [
|
|
889
833
|
{
|
|
@@ -898,6 +842,11 @@ ${JSON.stringify({
|
|
|
898
842
|
},
|
|
899
843
|
});
|
|
900
844
|
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
|
|
845
|
+
vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
|
|
846
|
+
originalTokenCount: 0,
|
|
847
|
+
newTokenCount: 0,
|
|
848
|
+
compressionStatus: CompressionStatus.COMPRESSED,
|
|
849
|
+
});
|
|
901
850
|
const mockStream = (async function* () {
|
|
902
851
|
yield { type: 'content', value: 'Hello' };
|
|
903
852
|
})();
|
|
@@ -914,7 +863,7 @@ ${JSON.stringify({
|
|
|
914
863
|
// consume stream
|
|
915
864
|
}
|
|
916
865
|
// Assert
|
|
917
|
-
expect(
|
|
866
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
918
867
|
const expectedContext = `
|
|
919
868
|
Here is the user's editor context as a JSON object. This is for your information only.
|
|
920
869
|
\`\`\`json
|
|
@@ -1110,6 +1059,91 @@ ${JSON.stringify({
|
|
|
1110
1059
|
console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
|
|
1111
1060
|
`${eventCount} events generated (properly bounded by MAX_TURNS)`);
|
|
1112
1061
|
});
|
|
1062
|
+
describe('Model Routing', () => {
|
|
1063
|
+
let mockRouterService;
|
|
1064
|
+
beforeEach(() => {
|
|
1065
|
+
mockRouterService = {
|
|
1066
|
+
route: vi
|
|
1067
|
+
.fn()
|
|
1068
|
+
.mockResolvedValue({ model: 'routed-model', reason: 'test' }),
|
|
1069
|
+
};
|
|
1070
|
+
vi.mocked(mockConfig.getModelRouterService).mockReturnValue(mockRouterService);
|
|
1071
|
+
mockTurnRunFn.mockReturnValue((async function* () {
|
|
1072
|
+
yield { type: 'content', value: 'Hello' };
|
|
1073
|
+
})());
|
|
1074
|
+
});
|
|
1075
|
+
it('should use the model router service to select a model on the first turn', async () => {
|
|
1076
|
+
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1077
|
+
await fromAsync(stream); // consume stream
|
|
1078
|
+
expect(mockConfig.getModelRouterService).toHaveBeenCalled();
|
|
1079
|
+
expect(mockRouterService.route).toHaveBeenCalled();
|
|
1080
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', // The model from the router
|
|
1081
|
+
[{ text: 'Hi' }], expect.any(Object));
|
|
1082
|
+
});
|
|
1083
|
+
it('should use the same model for subsequent turns in the same prompt (stickiness)', async () => {
|
|
1084
|
+
// First turn
|
|
1085
|
+
let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1086
|
+
await fromAsync(stream);
|
|
1087
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
|
1088
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
|
|
1089
|
+
// Second turn
|
|
1090
|
+
stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-1');
|
|
1091
|
+
await fromAsync(stream);
|
|
1092
|
+
// Router should not be called again
|
|
1093
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
|
1094
|
+
// Should stick to the first model
|
|
1095
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Continue' }], expect.any(Object));
|
|
1096
|
+
});
|
|
1097
|
+
it('should reset the sticky model and re-route when the prompt_id changes', async () => {
|
|
1098
|
+
// First prompt
|
|
1099
|
+
let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1100
|
+
await fromAsync(stream);
|
|
1101
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
|
1102
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
|
|
1103
|
+
// New prompt
|
|
1104
|
+
mockRouterService.route.mockResolvedValue({
|
|
1105
|
+
model: 'new-routed-model',
|
|
1106
|
+
reason: 'test',
|
|
1107
|
+
});
|
|
1108
|
+
stream = client.sendMessageStream([{ text: 'A new topic' }], new AbortController().signal, 'prompt-2');
|
|
1109
|
+
await fromAsync(stream);
|
|
1110
|
+
// Router should be called again for the new prompt
|
|
1111
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(2);
|
|
1112
|
+
// Should use the newly routed model
|
|
1113
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('new-routed-model', [{ text: 'A new topic' }], expect.any(Object));
|
|
1114
|
+
});
|
|
1115
|
+
it('should use the fallback model and bypass routing when in fallback mode', async () => {
|
|
1116
|
+
vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
|
|
1117
|
+
mockRouterService.route.mockResolvedValue({
|
|
1118
|
+
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
1119
|
+
reason: 'fallback',
|
|
1120
|
+
});
|
|
1121
|
+
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1122
|
+
await fromAsync(stream);
|
|
1123
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
|
|
1124
|
+
});
|
|
1125
|
+
it('should stick to the fallback model for the entire sequence even if fallback mode ends', async () => {
|
|
1126
|
+
// Start the sequence in fallback mode
|
|
1127
|
+
vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
|
|
1128
|
+
mockRouterService.route.mockResolvedValue({
|
|
1129
|
+
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
1130
|
+
reason: 'fallback',
|
|
1131
|
+
});
|
|
1132
|
+
let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-fallback-stickiness');
|
|
1133
|
+
await fromAsync(stream);
|
|
1134
|
+
// First call should use fallback model
|
|
1135
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
|
|
1136
|
+
// End fallback mode
|
|
1137
|
+
vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(false);
|
|
1138
|
+
// Second call in the same sequence
|
|
1139
|
+
stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-fallback-stickiness');
|
|
1140
|
+
await fromAsync(stream);
|
|
1141
|
+
// Router should still not be called, and it should stick to the fallback model
|
|
1142
|
+
expect(mockTurnRunFn).toHaveBeenCalledTimes(2); // Ensure it was called again
|
|
1143
|
+
expect(mockTurnRunFn).toHaveBeenLastCalledWith(DEFAULT_GEMINI_FLASH_MODEL, // Still the fallback model
|
|
1144
|
+
[{ text: 'Continue' }], expect.any(Object));
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1113
1147
|
describe('Editor context delta', () => {
|
|
1114
1148
|
const mockStream = (async function* () {
|
|
1115
1149
|
yield { type: 'content', value: 'Hello' };
|
|
@@ -1126,7 +1160,6 @@ ${JSON.stringify({
|
|
|
1126
1160
|
const mockChat = {
|
|
1127
1161
|
addHistory: vi.fn(),
|
|
1128
1162
|
setHistory: vi.fn(),
|
|
1129
|
-
sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
|
|
1130
1163
|
// Assume history is not empty for delta checks
|
|
1131
1164
|
getHistory: vi
|
|
1132
1165
|
.fn()
|
|
@@ -1250,7 +1283,7 @@ ${JSON.stringify({
|
|
|
1250
1283
|
},
|
|
1251
1284
|
};
|
|
1252
1285
|
// Setup current context
|
|
1253
|
-
vi.mocked(
|
|
1286
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1254
1287
|
workspaceState: {
|
|
1255
1288
|
openFiles: [
|
|
1256
1289
|
{ ...currentActiveFile, isActive: true, timestamp: Date.now() },
|
|
@@ -1296,7 +1329,7 @@ ${JSON.stringify({
|
|
|
1296
1329
|
},
|
|
1297
1330
|
};
|
|
1298
1331
|
// Setup current context (same as previous)
|
|
1299
|
-
vi.mocked(
|
|
1332
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1300
1333
|
workspaceState: {
|
|
1301
1334
|
openFiles: [
|
|
1302
1335
|
{ ...activeFile, isActive: true, timestamp: Date.now() },
|
|
@@ -1341,11 +1374,10 @@ ${JSON.stringify({
|
|
|
1341
1374
|
addHistory: vi.fn(),
|
|
1342
1375
|
getHistory: vi.fn().mockReturnValue([]), // Default empty history
|
|
1343
1376
|
setHistory: vi.fn(),
|
|
1344
|
-
sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
|
|
1345
1377
|
};
|
|
1346
1378
|
client['chat'] = mockChat;
|
|
1347
1379
|
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
|
|
1348
|
-
vi.mocked(
|
|
1380
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1349
1381
|
workspaceState: {
|
|
1350
1382
|
openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
|
|
1351
1383
|
},
|
|
@@ -1421,7 +1453,7 @@ ${JSON.stringify({
|
|
|
1421
1453
|
openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
|
|
1422
1454
|
},
|
|
1423
1455
|
};
|
|
1424
|
-
vi.mocked(
|
|
1456
|
+
vi.mocked(ideContextStore.get).mockReturnValue(initialIdeContext);
|
|
1425
1457
|
// Act: Send the tool response
|
|
1426
1458
|
let stream = client.sendMessageStream([
|
|
1427
1459
|
{
|
|
@@ -1467,7 +1499,7 @@ ${JSON.stringify({
|
|
|
1467
1499
|
openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
|
|
1468
1500
|
},
|
|
1469
1501
|
};
|
|
1470
|
-
vi.mocked(
|
|
1502
|
+
vi.mocked(ideContextStore.get).mockReturnValue(newIdeContext);
|
|
1471
1503
|
// Act: Send a new, regular user message
|
|
1472
1504
|
stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
|
|
1473
1505
|
for await (const _ of stream) {
|
|
@@ -1497,7 +1529,7 @@ ${JSON.stringify({
|
|
|
1497
1529
|
],
|
|
1498
1530
|
},
|
|
1499
1531
|
};
|
|
1500
|
-
vi.mocked(
|
|
1532
|
+
vi.mocked(ideContextStore.get).mockReturnValue(contextA);
|
|
1501
1533
|
// Act: Send a regular message to establish the initial context
|
|
1502
1534
|
let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
|
|
1503
1535
|
for await (const _ of stream) {
|
|
@@ -1530,7 +1562,7 @@ ${JSON.stringify({
|
|
|
1530
1562
|
],
|
|
1531
1563
|
},
|
|
1532
1564
|
};
|
|
1533
|
-
vi.mocked(
|
|
1565
|
+
vi.mocked(ideContextStore.get).mockReturnValue(contextB);
|
|
1534
1566
|
// Act: Send the tool response
|
|
1535
1567
|
stream = client.sendMessageStream([
|
|
1536
1568
|
{
|
|
@@ -1575,7 +1607,7 @@ ${JSON.stringify({
|
|
|
1575
1607
|
],
|
|
1576
1608
|
},
|
|
1577
1609
|
};
|
|
1578
|
-
vi.mocked(
|
|
1610
|
+
vi.mocked(ideContextStore.get).mockReturnValue(contextC);
|
|
1579
1611
|
// Act: Send a new, regular user message
|
|
1580
1612
|
stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
|
|
1581
1613
|
for await (const _ of stream) {
|
|
@@ -1640,6 +1672,35 @@ ${JSON.stringify({
|
|
|
1640
1672
|
// Assert
|
|
1641
1673
|
expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
|
|
1642
1674
|
});
|
|
1675
|
+
it('should abort linked signal when loop is detected', async () => {
|
|
1676
|
+
// Arrange
|
|
1677
|
+
vi.spyOn(client['loopDetector'], 'turnStarted').mockResolvedValue(false);
|
|
1678
|
+
vi.spyOn(client['loopDetector'], 'addAndCheck')
|
|
1679
|
+
.mockReturnValueOnce(false)
|
|
1680
|
+
.mockReturnValueOnce(true);
|
|
1681
|
+
let capturedSignal;
|
|
1682
|
+
mockTurnRunFn.mockImplementation((model, request, signal) => {
|
|
1683
|
+
capturedSignal = signal;
|
|
1684
|
+
return (async function* () {
|
|
1685
|
+
yield { type: 'content', value: 'First event' };
|
|
1686
|
+
yield { type: 'content', value: 'Second event' };
|
|
1687
|
+
})();
|
|
1688
|
+
});
|
|
1689
|
+
const mockChat = {
|
|
1690
|
+
addHistory: vi.fn(),
|
|
1691
|
+
getHistory: vi.fn().mockReturnValue([]),
|
|
1692
|
+
};
|
|
1693
|
+
client['chat'] = mockChat;
|
|
1694
|
+
// Act
|
|
1695
|
+
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop');
|
|
1696
|
+
const events = [];
|
|
1697
|
+
for await (const event of stream) {
|
|
1698
|
+
events.push(event);
|
|
1699
|
+
}
|
|
1700
|
+
// Assert
|
|
1701
|
+
expect(events).toContainEqual({ type: GeminiEventType.LoopDetected });
|
|
1702
|
+
expect(capturedSignal.aborted).toBe(true);
|
|
1703
|
+
});
|
|
1643
1704
|
});
|
|
1644
1705
|
describe('generateContent', () => {
|
|
1645
1706
|
it('should call generateContent with the correct parameters', async () => {
|