@google/gemini-cli-core 0.6.0-nightly.20250910.a31830a3 → 0.6.0-preview.10
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 +4 -4
- package/dist/index.js +4 -4
- 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 +37 -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 +4 -3
- package/dist/src/core/client.js +76 -141
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +218 -220
- 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 +13 -30
- package/dist/src/core/geminiChat.js +88 -230
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/geminiChat.test.js +268 -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 +192 -43
- package/dist/src/services/shellExecutionService.js.map +1 -1
- package/dist/src/services/shellExecutionService.test.js +184 -55
- package/dist/src/services/shellExecutionService.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +12 -2
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +100 -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 +13 -2
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +33 -2
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
- package/dist/src/telemetry/constants.d.ts +6 -0
- package/dist/src/telemetry/constants.js +6 -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 +7 -1
- package/dist/src/telemetry/loggers.js +97 -7
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/loggers.test.js +171 -37
- 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 +19 -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 +54 -3
- package/dist/src/telemetry/types.js +79 -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/tool-registry.js +1 -0
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tools.d.ts +8 -5
- package/dist/src/tools/tools.js +9 -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
|
@@ -13,7 +13,7 @@ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
|
|
13
13
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
|
14
14
|
import { setSimulate429 } from '../utils/testUtils.js';
|
|
15
15
|
import { tokenLimit } from './tokenLimits.js';
|
|
16
|
-
import {
|
|
16
|
+
import { ideContextStore } from '../ide/ideContext.js';
|
|
17
17
|
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
|
18
18
|
// Mock fs module to prevent actual file system operations during tests
|
|
19
19
|
const mockFileSystem = new Map();
|
|
@@ -108,22 +108,22 @@ describe('findIndexAfterFraction', () => {
|
|
|
108
108
|
// 0: 66
|
|
109
109
|
// 1: 66 + 68 = 134
|
|
110
110
|
// 2: 134 + 66 = 200
|
|
111
|
-
// 200 >= 166.5, so index is
|
|
112
|
-
expect(findIndexAfterFraction(history, 0.5)).toBe(
|
|
111
|
+
// 200 >= 166.5, so index is 3
|
|
112
|
+
expect(findIndexAfterFraction(history, 0.5)).toBe(3);
|
|
113
113
|
});
|
|
114
114
|
it('should handle a fraction that results in the last index', () => {
|
|
115
115
|
// 333 * 0.9 = 299.7
|
|
116
116
|
// ...
|
|
117
117
|
// 3: 200 + 68 = 268
|
|
118
118
|
// 4: 268 + 65 = 333
|
|
119
|
-
// 333 >= 299.7, so index is
|
|
120
|
-
expect(findIndexAfterFraction(history, 0.9)).toBe(
|
|
119
|
+
// 333 >= 299.7, so index is 5
|
|
120
|
+
expect(findIndexAfterFraction(history, 0.9)).toBe(5);
|
|
121
121
|
});
|
|
122
122
|
it('should handle an empty history', () => {
|
|
123
123
|
expect(findIndexAfterFraction([], 0.5)).toBe(0);
|
|
124
124
|
});
|
|
125
125
|
it('should handle a history with only one item', () => {
|
|
126
|
-
expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(
|
|
126
|
+
expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(1);
|
|
127
127
|
});
|
|
128
128
|
it('should handle history with weird parts', () => {
|
|
129
129
|
const historyWithEmptyParts = [
|
|
@@ -131,7 +131,7 @@ describe('findIndexAfterFraction', () => {
|
|
|
131
131
|
{ role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
|
|
132
132
|
{ role: 'user', parts: [{ text: 'Message 2' }] },
|
|
133
133
|
];
|
|
134
|
-
expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(
|
|
134
|
+
expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(2);
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
137
|
describe('isThinkingSupported', () => {
|
|
@@ -176,8 +176,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
176
176
|
mockContentGenerator = {
|
|
177
177
|
generateContent: mockGenerateContentFn,
|
|
178
178
|
generateContentStream: vi.fn(),
|
|
179
|
-
countTokens: vi.fn(),
|
|
180
|
-
embedContent: vi.fn(),
|
|
179
|
+
countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
|
|
181
180
|
batchEmbedContents: vi.fn(),
|
|
182
181
|
};
|
|
183
182
|
// Because the GeminiClient constructor kicks off an async process (startChat)
|
|
@@ -189,7 +188,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
189
188
|
};
|
|
190
189
|
const fileService = new FileDiscoveryService('/test/dir');
|
|
191
190
|
const contentGeneratorConfig = {
|
|
192
|
-
model: 'test-model',
|
|
193
191
|
apiKey: 'test-key',
|
|
194
192
|
vertexai: false,
|
|
195
193
|
authType: AuthType.USE_GEMINI,
|
|
@@ -222,16 +220,26 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
222
220
|
getDirectories: vi.fn().mockReturnValue(['/test/dir']),
|
|
223
221
|
}),
|
|
224
222
|
getGeminiClient: vi.fn(),
|
|
223
|
+
getModelRouterService: vi.fn().mockReturnValue({
|
|
224
|
+
route: vi.fn().mockResolvedValue({ model: 'default-routed-model' }),
|
|
225
|
+
}),
|
|
225
226
|
isInFallbackMode: vi.fn().mockReturnValue(false),
|
|
226
227
|
setFallbackMode: vi.fn(),
|
|
227
228
|
getChatCompression: vi.fn().mockReturnValue(undefined),
|
|
228
229
|
getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
|
|
229
230
|
getUseSmartEdit: vi.fn().mockReturnValue(false),
|
|
231
|
+
getUseModelRouter: vi.fn().mockReturnValue(false),
|
|
230
232
|
getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
|
|
231
233
|
storage: {
|
|
232
234
|
getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
|
|
233
235
|
},
|
|
234
236
|
getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
|
|
237
|
+
getBaseLlmClient: vi.fn().mockReturnValue({
|
|
238
|
+
generateJson: vi.fn().mockResolvedValue({
|
|
239
|
+
next_speaker: 'user',
|
|
240
|
+
reasoning: 'test',
|
|
241
|
+
}),
|
|
242
|
+
}),
|
|
235
243
|
};
|
|
236
244
|
client = new GeminiClient(mockConfig);
|
|
237
245
|
await client.initialize();
|
|
@@ -240,129 +248,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
240
248
|
afterEach(() => {
|
|
241
249
|
vi.restoreAllMocks();
|
|
242
250
|
});
|
|
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
251
|
describe('addHistory', () => {
|
|
367
252
|
it('should call chat.addHistory with the provided content', async () => {
|
|
368
253
|
const mockChat = {
|
|
@@ -400,7 +285,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
400
285
|
});
|
|
401
286
|
});
|
|
402
287
|
describe('tryCompressChat', () => {
|
|
403
|
-
const mockSendMessage = vi.fn();
|
|
404
288
|
const mockGetHistory = vi.fn();
|
|
405
289
|
beforeEach(() => {
|
|
406
290
|
vi.mock('./tokenLimits', () => ({
|
|
@@ -410,7 +294,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
410
294
|
getHistory: mockGetHistory,
|
|
411
295
|
addHistory: vi.fn(),
|
|
412
296
|
setHistory: vi.fn(),
|
|
413
|
-
sendMessage: mockSendMessage,
|
|
414
297
|
};
|
|
415
298
|
});
|
|
416
299
|
function setup({ chatHistory = [
|
|
@@ -420,7 +303,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
420
303
|
const mockChat = {
|
|
421
304
|
getHistory: vi.fn().mockReturnValue(chatHistory),
|
|
422
305
|
setHistory: vi.fn(),
|
|
423
|
-
sendMessage: vi.fn().mockResolvedValue({ text: 'Summary' }),
|
|
424
306
|
};
|
|
425
307
|
vi.mocked(mockContentGenerator.countTokens)
|
|
426
308
|
.mockResolvedValueOnce({ totalTokens: 1000 })
|
|
@@ -435,7 +317,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
435
317
|
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
436
318
|
totalTokens: 1000,
|
|
437
319
|
});
|
|
438
|
-
await client.tryCompressChat('prompt-id-4'); // Fails
|
|
320
|
+
await client.tryCompressChat('prompt-id-4', false); // Fails
|
|
439
321
|
const result = await client.tryCompressChat('prompt-id-4', true);
|
|
440
322
|
expect(result).toEqual({
|
|
441
323
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
@@ -448,7 +330,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
448
330
|
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
449
331
|
totalTokens: 1000,
|
|
450
332
|
});
|
|
451
|
-
const result = await client.tryCompressChat('prompt-id-4',
|
|
333
|
+
const result = await client.tryCompressChat('prompt-id-4', false);
|
|
452
334
|
expect(result).toEqual({
|
|
453
335
|
compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
|
454
336
|
newTokenCount: 5000,
|
|
@@ -457,7 +339,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
457
339
|
});
|
|
458
340
|
it('does not manipulate the source chat', async () => {
|
|
459
341
|
const { client, mockChat } = setup();
|
|
460
|
-
await client.tryCompressChat('prompt-id-4',
|
|
342
|
+
await client.tryCompressChat('prompt-id-4', false);
|
|
461
343
|
expect(client['chat']).toBe(mockChat); // a new chat session was not created
|
|
462
344
|
});
|
|
463
345
|
it('restores the history back to the original', async () => {
|
|
@@ -473,14 +355,14 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
473
355
|
const { client } = setup({
|
|
474
356
|
chatHistory: originalHistory,
|
|
475
357
|
});
|
|
476
|
-
const { compressionStatus } = await client.tryCompressChat('prompt-id-4');
|
|
358
|
+
const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false);
|
|
477
359
|
expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
|
|
478
360
|
expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
|
|
479
361
|
});
|
|
480
362
|
it('will not attempt to compress context after a failure', async () => {
|
|
481
363
|
const { client } = setup();
|
|
482
|
-
await client.tryCompressChat('prompt-id-4');
|
|
483
|
-
const result = await client.tryCompressChat('prompt-id-5');
|
|
364
|
+
await client.tryCompressChat('prompt-id-4', false);
|
|
365
|
+
const result = await client.tryCompressChat('prompt-id-5', false);
|
|
484
366
|
// it counts tokens for {original, compressed} and then never again
|
|
485
367
|
expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
|
|
486
368
|
expect(result).toEqual({
|
|
@@ -500,7 +382,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
500
382
|
totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
|
|
501
383
|
});
|
|
502
384
|
const initialChat = client.getChat();
|
|
503
|
-
const result = await client.tryCompressChat('prompt-id-2');
|
|
385
|
+
const result = await client.tryCompressChat('prompt-id-2', false);
|
|
504
386
|
const newChat = client.getChat();
|
|
505
387
|
expect(tokenLimit).toHaveBeenCalled();
|
|
506
388
|
expect(result).toEqual({
|
|
@@ -527,11 +409,17 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
527
409
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
|
|
528
410
|
.mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
|
|
529
411
|
// Mock the summary response from the chat
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
412
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
413
|
+
candidates: [
|
|
414
|
+
{
|
|
415
|
+
content: {
|
|
416
|
+
role: 'model',
|
|
417
|
+
parts: [{ text: 'This is a summary.' }],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
],
|
|
533
421
|
});
|
|
534
|
-
await client.tryCompressChat('prompt-id-3');
|
|
422
|
+
await client.tryCompressChat('prompt-id-3', false);
|
|
535
423
|
expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
|
|
536
424
|
tokens_before: originalTokenCount,
|
|
537
425
|
tokens_after: newTokenCount,
|
|
@@ -553,15 +441,21 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
553
441
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
|
|
554
442
|
.mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
|
|
555
443
|
// Mock the summary response from the chat
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
444
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
445
|
+
candidates: [
|
|
446
|
+
{
|
|
447
|
+
content: {
|
|
448
|
+
role: 'model',
|
|
449
|
+
parts: [{ text: 'This is a summary.' }],
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
],
|
|
559
453
|
});
|
|
560
454
|
const initialChat = client.getChat();
|
|
561
|
-
const result = await client.tryCompressChat('prompt-id-3');
|
|
455
|
+
const result = await client.tryCompressChat('prompt-id-3', false);
|
|
562
456
|
const newChat = client.getChat();
|
|
563
457
|
expect(tokenLimit).toHaveBeenCalled();
|
|
564
|
-
expect(
|
|
458
|
+
expect(mockGenerateContentFn).toHaveBeenCalled();
|
|
565
459
|
// Assert that summarization happened and returned the correct stats
|
|
566
460
|
expect(result).toEqual({
|
|
567
461
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
@@ -598,15 +492,21 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
598
492
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
|
|
599
493
|
.mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
|
|
600
494
|
// Mock the summary response from the chat
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
495
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
496
|
+
candidates: [
|
|
497
|
+
{
|
|
498
|
+
content: {
|
|
499
|
+
role: 'model',
|
|
500
|
+
parts: [{ text: 'This is a summary.' }],
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
],
|
|
604
504
|
});
|
|
605
505
|
const initialChat = client.getChat();
|
|
606
|
-
const result = await client.tryCompressChat('prompt-id-3');
|
|
506
|
+
const result = await client.tryCompressChat('prompt-id-3', false);
|
|
607
507
|
const newChat = client.getChat();
|
|
608
508
|
expect(tokenLimit).toHaveBeenCalled();
|
|
609
|
-
expect(
|
|
509
|
+
expect(mockGenerateContentFn).toHaveBeenCalled();
|
|
610
510
|
// Assert that summarization happened and returned the correct stats
|
|
611
511
|
expect(result).toEqual({
|
|
612
512
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
@@ -632,14 +532,20 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
632
532
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount })
|
|
633
533
|
.mockResolvedValueOnce({ totalTokens: newTokenCount });
|
|
634
534
|
// Mock the summary response from the chat
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
535
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
536
|
+
candidates: [
|
|
537
|
+
{
|
|
538
|
+
content: {
|
|
539
|
+
role: 'model',
|
|
540
|
+
parts: [{ text: 'This is a summary.' }],
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
],
|
|
638
544
|
});
|
|
639
545
|
const initialChat = client.getChat();
|
|
640
|
-
const result = await client.tryCompressChat('prompt-id-1',
|
|
546
|
+
const result = await client.tryCompressChat('prompt-id-1', false); // force = true
|
|
641
547
|
const newChat = client.getChat();
|
|
642
|
-
expect(
|
|
548
|
+
expect(mockGenerateContentFn).toHaveBeenCalled();
|
|
643
549
|
expect(result).toEqual({
|
|
644
550
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
645
551
|
originalTokenCount,
|
|
@@ -648,45 +554,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
648
554
|
// Assert that the chat was reset
|
|
649
555
|
expect(newChat).not.toBe(initialChat);
|
|
650
556
|
});
|
|
651
|
-
it('should use current model from config for token counting after sendMessage', async () => {
|
|
652
|
-
const initialModel = mockConfig.getModel();
|
|
653
|
-
// mock the model has been changed between calls of `countTokens`
|
|
654
|
-
const firstCurrentModel = initialModel + '-changed-1';
|
|
655
|
-
const secondCurrentModel = initialModel + '-changed-2';
|
|
656
|
-
vi.mocked(mockConfig.getModel)
|
|
657
|
-
.mockReturnValueOnce(firstCurrentModel)
|
|
658
|
-
.mockReturnValueOnce(secondCurrentModel);
|
|
659
|
-
vi.mocked(mockContentGenerator.countTokens)
|
|
660
|
-
.mockResolvedValueOnce({ totalTokens: 100000 })
|
|
661
|
-
.mockResolvedValueOnce({ totalTokens: 5000 });
|
|
662
|
-
const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
|
|
663
|
-
const mockChatHistory = [
|
|
664
|
-
{ role: 'user', parts: [{ text: 'Long conversation' }] },
|
|
665
|
-
{ role: 'model', parts: [{ text: 'Long response' }] },
|
|
666
|
-
];
|
|
667
|
-
const mockChat = {
|
|
668
|
-
getHistory: vi.fn().mockReturnValue(mockChatHistory),
|
|
669
|
-
setHistory: vi.fn(),
|
|
670
|
-
sendMessage: mockSendMessage,
|
|
671
|
-
};
|
|
672
|
-
client['chat'] = mockChat;
|
|
673
|
-
client['startChat'] = vi.fn().mockResolvedValue(mockChat);
|
|
674
|
-
const result = await client.tryCompressChat('prompt-id-4', true);
|
|
675
|
-
expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
|
|
676
|
-
expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(1, {
|
|
677
|
-
model: firstCurrentModel,
|
|
678
|
-
contents: mockChatHistory,
|
|
679
|
-
});
|
|
680
|
-
expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(2, {
|
|
681
|
-
model: secondCurrentModel,
|
|
682
|
-
contents: expect.any(Array),
|
|
683
|
-
});
|
|
684
|
-
expect(result).toEqual({
|
|
685
|
-
compressionStatus: CompressionStatus.COMPRESSED,
|
|
686
|
-
originalTokenCount: 100000,
|
|
687
|
-
newTokenCount: 5000,
|
|
688
|
-
});
|
|
689
|
-
});
|
|
690
557
|
});
|
|
691
558
|
describe('sendMessageStream', () => {
|
|
692
559
|
it('emits a compression event when the context was automatically compressed', async () => {
|
|
@@ -740,7 +607,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
740
607
|
});
|
|
741
608
|
it('should include editor context when ideMode is enabled', async () => {
|
|
742
609
|
// Arrange
|
|
743
|
-
vi.mocked(
|
|
610
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
744
611
|
workspaceState: {
|
|
745
612
|
openFiles: [
|
|
746
613
|
{
|
|
@@ -762,6 +629,11 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
762
629
|
},
|
|
763
630
|
});
|
|
764
631
|
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
|
|
632
|
+
vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
|
|
633
|
+
originalTokenCount: 0,
|
|
634
|
+
newTokenCount: 0,
|
|
635
|
+
compressionStatus: CompressionStatus.COMPRESSED,
|
|
636
|
+
});
|
|
765
637
|
mockTurnRunFn.mockReturnValue((async function* () {
|
|
766
638
|
yield { type: 'content', value: 'Hello' };
|
|
767
639
|
})());
|
|
@@ -777,7 +649,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
777
649
|
// consume stream
|
|
778
650
|
}
|
|
779
651
|
// Assert
|
|
780
|
-
expect(
|
|
652
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
781
653
|
const expectedContext = `
|
|
782
654
|
Here is the user's editor context as a JSON object. This is for your information only.
|
|
783
655
|
\`\`\`json
|
|
@@ -802,7 +674,7 @@ ${JSON.stringify({
|
|
|
802
674
|
});
|
|
803
675
|
it('should not add context if ideMode is enabled but no open files', async () => {
|
|
804
676
|
// Arrange
|
|
805
|
-
vi.mocked(
|
|
677
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
806
678
|
workspaceState: {
|
|
807
679
|
openFiles: [],
|
|
808
680
|
},
|
|
@@ -824,12 +696,16 @@ ${JSON.stringify({
|
|
|
824
696
|
// consume stream
|
|
825
697
|
}
|
|
826
698
|
// Assert
|
|
827
|
-
expect(
|
|
828
|
-
|
|
699
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
700
|
+
// The `turn.run` method is now called with the model name as the first
|
|
701
|
+
// argument. We use `expect.any(String)` because this test is
|
|
702
|
+
// concerned with the IDE context logic, not the model routing,
|
|
703
|
+
// which is tested in its own dedicated suite.
|
|
704
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith(expect.any(String), initialRequest, expect.any(Object));
|
|
829
705
|
});
|
|
830
706
|
it('should add context if ideMode is enabled and there is one active file', async () => {
|
|
831
707
|
// Arrange
|
|
832
|
-
vi.mocked(
|
|
708
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
833
709
|
workspaceState: {
|
|
834
710
|
openFiles: [
|
|
835
711
|
{
|
|
@@ -843,6 +719,11 @@ ${JSON.stringify({
|
|
|
843
719
|
},
|
|
844
720
|
});
|
|
845
721
|
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
|
|
722
|
+
vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
|
|
723
|
+
originalTokenCount: 0,
|
|
724
|
+
newTokenCount: 0,
|
|
725
|
+
compressionStatus: CompressionStatus.COMPRESSED,
|
|
726
|
+
});
|
|
846
727
|
const mockStream = (async function* () {
|
|
847
728
|
yield { type: 'content', value: 'Hello' };
|
|
848
729
|
})();
|
|
@@ -859,7 +740,7 @@ ${JSON.stringify({
|
|
|
859
740
|
// consume stream
|
|
860
741
|
}
|
|
861
742
|
// Assert
|
|
862
|
-
expect(
|
|
743
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
863
744
|
const expectedContext = `
|
|
864
745
|
Here is the user's editor context as a JSON object. This is for your information only.
|
|
865
746
|
\`\`\`json
|
|
@@ -883,7 +764,7 @@ ${JSON.stringify({
|
|
|
883
764
|
});
|
|
884
765
|
it('should add context if ideMode is enabled and there are open files but no active file', async () => {
|
|
885
766
|
// Arrange
|
|
886
|
-
vi.mocked(
|
|
767
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
887
768
|
workspaceState: {
|
|
888
769
|
openFiles: [
|
|
889
770
|
{
|
|
@@ -898,6 +779,11 @@ ${JSON.stringify({
|
|
|
898
779
|
},
|
|
899
780
|
});
|
|
900
781
|
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
|
|
782
|
+
vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
|
|
783
|
+
originalTokenCount: 0,
|
|
784
|
+
newTokenCount: 0,
|
|
785
|
+
compressionStatus: CompressionStatus.COMPRESSED,
|
|
786
|
+
});
|
|
901
787
|
const mockStream = (async function* () {
|
|
902
788
|
yield { type: 'content', value: 'Hello' };
|
|
903
789
|
})();
|
|
@@ -914,7 +800,7 @@ ${JSON.stringify({
|
|
|
914
800
|
// consume stream
|
|
915
801
|
}
|
|
916
802
|
// Assert
|
|
917
|
-
expect(
|
|
803
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
918
804
|
const expectedContext = `
|
|
919
805
|
Here is the user's editor context as a JSON object. This is for your information only.
|
|
920
806
|
\`\`\`json
|
|
@@ -1110,6 +996,91 @@ ${JSON.stringify({
|
|
|
1110
996
|
console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
|
|
1111
997
|
`${eventCount} events generated (properly bounded by MAX_TURNS)`);
|
|
1112
998
|
});
|
|
999
|
+
describe('Model Routing', () => {
|
|
1000
|
+
let mockRouterService;
|
|
1001
|
+
beforeEach(() => {
|
|
1002
|
+
mockRouterService = {
|
|
1003
|
+
route: vi
|
|
1004
|
+
.fn()
|
|
1005
|
+
.mockResolvedValue({ model: 'routed-model', reason: 'test' }),
|
|
1006
|
+
};
|
|
1007
|
+
vi.mocked(mockConfig.getModelRouterService).mockReturnValue(mockRouterService);
|
|
1008
|
+
mockTurnRunFn.mockReturnValue((async function* () {
|
|
1009
|
+
yield { type: 'content', value: 'Hello' };
|
|
1010
|
+
})());
|
|
1011
|
+
});
|
|
1012
|
+
it('should use the model router service to select a model on the first turn', async () => {
|
|
1013
|
+
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1014
|
+
await fromAsync(stream); // consume stream
|
|
1015
|
+
expect(mockConfig.getModelRouterService).toHaveBeenCalled();
|
|
1016
|
+
expect(mockRouterService.route).toHaveBeenCalled();
|
|
1017
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', // The model from the router
|
|
1018
|
+
[{ text: 'Hi' }], expect.any(Object));
|
|
1019
|
+
});
|
|
1020
|
+
it('should use the same model for subsequent turns in the same prompt (stickiness)', async () => {
|
|
1021
|
+
// First turn
|
|
1022
|
+
let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1023
|
+
await fromAsync(stream);
|
|
1024
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
|
1025
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
|
|
1026
|
+
// Second turn
|
|
1027
|
+
stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-1');
|
|
1028
|
+
await fromAsync(stream);
|
|
1029
|
+
// Router should not be called again
|
|
1030
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
|
1031
|
+
// Should stick to the first model
|
|
1032
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Continue' }], expect.any(Object));
|
|
1033
|
+
});
|
|
1034
|
+
it('should reset the sticky model and re-route when the prompt_id changes', async () => {
|
|
1035
|
+
// First prompt
|
|
1036
|
+
let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1037
|
+
await fromAsync(stream);
|
|
1038
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
|
1039
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
|
|
1040
|
+
// New prompt
|
|
1041
|
+
mockRouterService.route.mockResolvedValue({
|
|
1042
|
+
model: 'new-routed-model',
|
|
1043
|
+
reason: 'test',
|
|
1044
|
+
});
|
|
1045
|
+
stream = client.sendMessageStream([{ text: 'A new topic' }], new AbortController().signal, 'prompt-2');
|
|
1046
|
+
await fromAsync(stream);
|
|
1047
|
+
// Router should be called again for the new prompt
|
|
1048
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(2);
|
|
1049
|
+
// Should use the newly routed model
|
|
1050
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('new-routed-model', [{ text: 'A new topic' }], expect.any(Object));
|
|
1051
|
+
});
|
|
1052
|
+
it('should use the fallback model and bypass routing when in fallback mode', async () => {
|
|
1053
|
+
vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
|
|
1054
|
+
mockRouterService.route.mockResolvedValue({
|
|
1055
|
+
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
1056
|
+
reason: 'fallback',
|
|
1057
|
+
});
|
|
1058
|
+
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1059
|
+
await fromAsync(stream);
|
|
1060
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
|
|
1061
|
+
});
|
|
1062
|
+
it('should stick to the fallback model for the entire sequence even if fallback mode ends', async () => {
|
|
1063
|
+
// Start the sequence in fallback mode
|
|
1064
|
+
vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
|
|
1065
|
+
mockRouterService.route.mockResolvedValue({
|
|
1066
|
+
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
1067
|
+
reason: 'fallback',
|
|
1068
|
+
});
|
|
1069
|
+
let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-fallback-stickiness');
|
|
1070
|
+
await fromAsync(stream);
|
|
1071
|
+
// First call should use fallback model
|
|
1072
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
|
|
1073
|
+
// End fallback mode
|
|
1074
|
+
vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(false);
|
|
1075
|
+
// Second call in the same sequence
|
|
1076
|
+
stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-fallback-stickiness');
|
|
1077
|
+
await fromAsync(stream);
|
|
1078
|
+
// Router should still not be called, and it should stick to the fallback model
|
|
1079
|
+
expect(mockTurnRunFn).toHaveBeenCalledTimes(2); // Ensure it was called again
|
|
1080
|
+
expect(mockTurnRunFn).toHaveBeenLastCalledWith(DEFAULT_GEMINI_FLASH_MODEL, // Still the fallback model
|
|
1081
|
+
[{ text: 'Continue' }], expect.any(Object));
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1113
1084
|
describe('Editor context delta', () => {
|
|
1114
1085
|
const mockStream = (async function* () {
|
|
1115
1086
|
yield { type: 'content', value: 'Hello' };
|
|
@@ -1126,7 +1097,6 @@ ${JSON.stringify({
|
|
|
1126
1097
|
const mockChat = {
|
|
1127
1098
|
addHistory: vi.fn(),
|
|
1128
1099
|
setHistory: vi.fn(),
|
|
1129
|
-
sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
|
|
1130
1100
|
// Assume history is not empty for delta checks
|
|
1131
1101
|
getHistory: vi
|
|
1132
1102
|
.fn()
|
|
@@ -1250,7 +1220,7 @@ ${JSON.stringify({
|
|
|
1250
1220
|
},
|
|
1251
1221
|
};
|
|
1252
1222
|
// Setup current context
|
|
1253
|
-
vi.mocked(
|
|
1223
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1254
1224
|
workspaceState: {
|
|
1255
1225
|
openFiles: [
|
|
1256
1226
|
{ ...currentActiveFile, isActive: true, timestamp: Date.now() },
|
|
@@ -1296,7 +1266,7 @@ ${JSON.stringify({
|
|
|
1296
1266
|
},
|
|
1297
1267
|
};
|
|
1298
1268
|
// Setup current context (same as previous)
|
|
1299
|
-
vi.mocked(
|
|
1269
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1300
1270
|
workspaceState: {
|
|
1301
1271
|
openFiles: [
|
|
1302
1272
|
{ ...activeFile, isActive: true, timestamp: Date.now() },
|
|
@@ -1341,11 +1311,10 @@ ${JSON.stringify({
|
|
|
1341
1311
|
addHistory: vi.fn(),
|
|
1342
1312
|
getHistory: vi.fn().mockReturnValue([]), // Default empty history
|
|
1343
1313
|
setHistory: vi.fn(),
|
|
1344
|
-
sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
|
|
1345
1314
|
};
|
|
1346
1315
|
client['chat'] = mockChat;
|
|
1347
1316
|
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
|
|
1348
|
-
vi.mocked(
|
|
1317
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1349
1318
|
workspaceState: {
|
|
1350
1319
|
openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
|
|
1351
1320
|
},
|
|
@@ -1421,7 +1390,7 @@ ${JSON.stringify({
|
|
|
1421
1390
|
openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
|
|
1422
1391
|
},
|
|
1423
1392
|
};
|
|
1424
|
-
vi.mocked(
|
|
1393
|
+
vi.mocked(ideContextStore.get).mockReturnValue(initialIdeContext);
|
|
1425
1394
|
// Act: Send the tool response
|
|
1426
1395
|
let stream = client.sendMessageStream([
|
|
1427
1396
|
{
|
|
@@ -1467,7 +1436,7 @@ ${JSON.stringify({
|
|
|
1467
1436
|
openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
|
|
1468
1437
|
},
|
|
1469
1438
|
};
|
|
1470
|
-
vi.mocked(
|
|
1439
|
+
vi.mocked(ideContextStore.get).mockReturnValue(newIdeContext);
|
|
1471
1440
|
// Act: Send a new, regular user message
|
|
1472
1441
|
stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
|
|
1473
1442
|
for await (const _ of stream) {
|
|
@@ -1497,7 +1466,7 @@ ${JSON.stringify({
|
|
|
1497
1466
|
],
|
|
1498
1467
|
},
|
|
1499
1468
|
};
|
|
1500
|
-
vi.mocked(
|
|
1469
|
+
vi.mocked(ideContextStore.get).mockReturnValue(contextA);
|
|
1501
1470
|
// Act: Send a regular message to establish the initial context
|
|
1502
1471
|
let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
|
|
1503
1472
|
for await (const _ of stream) {
|
|
@@ -1530,7 +1499,7 @@ ${JSON.stringify({
|
|
|
1530
1499
|
],
|
|
1531
1500
|
},
|
|
1532
1501
|
};
|
|
1533
|
-
vi.mocked(
|
|
1502
|
+
vi.mocked(ideContextStore.get).mockReturnValue(contextB);
|
|
1534
1503
|
// Act: Send the tool response
|
|
1535
1504
|
stream = client.sendMessageStream([
|
|
1536
1505
|
{
|
|
@@ -1575,7 +1544,7 @@ ${JSON.stringify({
|
|
|
1575
1544
|
],
|
|
1576
1545
|
},
|
|
1577
1546
|
};
|
|
1578
|
-
vi.mocked(
|
|
1547
|
+
vi.mocked(ideContextStore.get).mockReturnValue(contextC);
|
|
1579
1548
|
// Act: Send a new, regular user message
|
|
1580
1549
|
stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
|
|
1581
1550
|
for await (const _ of stream) {
|
|
@@ -1640,6 +1609,35 @@ ${JSON.stringify({
|
|
|
1640
1609
|
// Assert
|
|
1641
1610
|
expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
|
|
1642
1611
|
});
|
|
1612
|
+
it('should abort linked signal when loop is detected', async () => {
|
|
1613
|
+
// Arrange
|
|
1614
|
+
vi.spyOn(client['loopDetector'], 'turnStarted').mockResolvedValue(false);
|
|
1615
|
+
vi.spyOn(client['loopDetector'], 'addAndCheck')
|
|
1616
|
+
.mockReturnValueOnce(false)
|
|
1617
|
+
.mockReturnValueOnce(true);
|
|
1618
|
+
let capturedSignal;
|
|
1619
|
+
mockTurnRunFn.mockImplementation((model, request, signal) => {
|
|
1620
|
+
capturedSignal = signal;
|
|
1621
|
+
return (async function* () {
|
|
1622
|
+
yield { type: 'content', value: 'First event' };
|
|
1623
|
+
yield { type: 'content', value: 'Second event' };
|
|
1624
|
+
})();
|
|
1625
|
+
});
|
|
1626
|
+
const mockChat = {
|
|
1627
|
+
addHistory: vi.fn(),
|
|
1628
|
+
getHistory: vi.fn().mockReturnValue([]),
|
|
1629
|
+
};
|
|
1630
|
+
client['chat'] = mockChat;
|
|
1631
|
+
// Act
|
|
1632
|
+
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop');
|
|
1633
|
+
const events = [];
|
|
1634
|
+
for await (const event of stream) {
|
|
1635
|
+
events.push(event);
|
|
1636
|
+
}
|
|
1637
|
+
// Assert
|
|
1638
|
+
expect(events).toContainEqual({ type: GeminiEventType.LoopDetected });
|
|
1639
|
+
expect(capturedSignal.aborted).toBe(true);
|
|
1640
|
+
});
|
|
1643
1641
|
});
|
|
1644
1642
|
describe('generateContent', () => {
|
|
1645
1643
|
it('should call generateContent with the correct parameters', async () => {
|