@google/gemini-cli-core 0.0.3-preview.4 → 0.0.4
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 +6 -2
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/src/code_assist/codeAssist.d.ts +2 -0
- package/dist/src/code_assist/codeAssist.js +12 -0
- package/dist/src/code_assist/codeAssist.js.map +1 -1
- package/dist/src/code_assist/converter.d.ts +3 -1
- package/dist/src/code_assist/converter.js +2 -1
- 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 +25 -0
- package/dist/src/code_assist/oauth-credential-storage.js +109 -0
- package/dist/src/code_assist/oauth-credential-storage.js.map +1 -0
- package/dist/src/code_assist/oauth-credential-storage.test.js +136 -0
- package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -0
- package/dist/src/code_assist/oauth2.js +92 -29
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/oauth2.test.js +729 -339
- package/dist/src/code_assist/oauth2.test.js.map +1 -1
- package/dist/src/code_assist/server.d.ts +1 -1
- package/dist/src/code_assist/server.js +24 -1
- package/dist/src/code_assist/server.js.map +1 -1
- package/dist/src/code_assist/server.test.js +25 -0
- package/dist/src/code_assist/server.test.js.map +1 -1
- package/dist/src/code_assist/types.d.ts +17 -2
- package/dist/src/config/config.d.ts +72 -12
- package/dist/src/config/config.js +196 -64
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +305 -178
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/models.d.ts +16 -0
- package/dist/src/config/models.js +29 -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/config/storage.d.ts +2 -0
- package/dist/src/config/storage.js +6 -1
- package/dist/src/config/storage.js.map +1 -1
- package/dist/src/config/storage.test.js +4 -0
- package/dist/src/config/storage.test.js.map +1 -1
- 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 +46 -0
- package/dist/src/core/baseLlmClient.js +112 -0
- package/dist/src/core/baseLlmClient.js.map +1 -0
- package/dist/src/core/baseLlmClient.test.d.ts +6 -0
- package/dist/src/core/baseLlmClient.test.js +253 -0
- package/dist/src/core/baseLlmClient.test.js.map +1 -0
- package/dist/src/core/client.d.ts +16 -21
- package/dist/src/core/client.js +145 -232
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +393 -492
- package/dist/src/core/client.test.js.map +1 -1
- package/dist/src/core/contentGenerator.d.ts +2 -3
- package/dist/src/core/contentGenerator.js +0 -4
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/contentGenerator.test.js +1 -3
- package/dist/src/core/contentGenerator.test.js.map +1 -1
- package/dist/src/core/coreToolScheduler.d.ts +8 -3
- package/dist/src/core/coreToolScheduler.js +106 -5
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/coreToolScheduler.test.js +233 -5
- package/dist/src/core/coreToolScheduler.test.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +38 -32
- package/dist/src/core/geminiChat.js +209 -219
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/geminiChat.test.js +674 -386
- package/dist/src/core/geminiChat.test.js.map +1 -1
- package/dist/src/core/loggingContentGenerator.js +13 -16
- package/dist/src/core/loggingContentGenerator.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.test.js +59 -1
- package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
- package/dist/src/core/prompts.d.ts +5 -0
- package/dist/src/core/prompts.js +63 -42
- package/dist/src/core/prompts.js.map +1 -1
- package/dist/src/core/prompts.test.js +130 -1
- package/dist/src/core/prompts.test.js.map +1 -1
- package/dist/src/core/subagent.js +7 -10
- package/dist/src/core/subagent.js.map +1 -1
- package/dist/src/core/subagent.test.js +32 -22
- package/dist/src/core/subagent.test.js.map +1 -1
- package/dist/src/core/turn.d.ts +21 -5
- package/dist/src/core/turn.js +45 -11
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/core/turn.test.js +340 -100
- package/dist/src/core/turn.test.js.map +1 -1
- package/dist/src/fallback/handler.d.ts +7 -0
- package/dist/src/fallback/handler.js +51 -0
- package/dist/src/fallback/handler.js.map +1 -0
- package/dist/src/fallback/handler.test.d.ts +6 -0
- package/dist/src/fallback/handler.test.js +130 -0
- package/dist/src/fallback/handler.test.js.map +1 -0
- package/dist/src/fallback/types.d.ts +14 -0
- package/dist/src/fallback/types.js +7 -0
- package/dist/src/fallback/types.js.map +1 -0
- 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 +3 -0
- package/dist/src/ide/constants.js +3 -0
- package/dist/src/ide/constants.js.map +1 -1
- package/dist/src/ide/detect-ide.d.ts +42 -14
- package/dist/src/ide/detect-ide.js +22 -68
- package/dist/src/ide/detect-ide.js.map +1 -1
- package/dist/src/ide/detect-ide.test.js +11 -51
- package/dist/src/ide/detect-ide.test.js.map +1 -1
- package/dist/src/ide/ide-client.d.ts +60 -18
- package/dist/src/ide/ide-client.js +275 -53
- package/dist/src/ide/ide-client.js.map +1 -1
- package/dist/src/ide/ide-client.test.js +239 -6
- package/dist/src/ide/ide-client.test.js.map +1 -1
- package/dist/src/ide/ide-installer.d.ts +2 -2
- package/dist/src/ide/ide-installer.js +15 -11
- package/dist/src/ide/ide-installer.js.map +1 -1
- package/dist/src/ide/ide-installer.test.js +30 -12
- package/dist/src/ide/ide-installer.test.js.map +1 -1
- package/dist/src/ide/ideContext.d.ts +35 -365
- package/dist/src/ide/ideContext.js +60 -106
- package/dist/src/ide/ideContext.js.map +1 -1
- package/dist/src/ide/ideContext.test.js +152 -24
- package/dist/src/ide/ideContext.test.js.map +1 -1
- package/dist/src/ide/process-utils.d.ts +0 -1
- package/dist/src/ide/process-utils.js +43 -25
- package/dist/src/ide/process-utils.js.map +1 -1
- package/dist/src/ide/process-utils.test.js +90 -4
- package/dist/src/ide/process-utils.test.js.map +1 -1
- package/dist/src/ide/types.d.ts +486 -0
- package/dist/src/ide/types.js +138 -0
- package/dist/src/ide/types.js.map +1 -0
- package/dist/src/index.d.ts +10 -2
- package/dist/src/index.js +11 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/oauth-provider.d.ts +15 -12
- package/dist/src/mcp/oauth-provider.js +63 -56
- package/dist/src/mcp/oauth-provider.js.map +1 -1
- package/dist/src/mcp/oauth-provider.test.js +74 -35
- package/dist/src/mcp/oauth-provider.test.js.map +1 -1
- package/dist/src/mcp/oauth-token-storage.d.ts +14 -10
- package/dist/src/mcp/oauth-token-storage.js +52 -20
- package/dist/src/mcp/oauth-token-storage.js.map +1 -1
- package/dist/src/mcp/oauth-token-storage.test.js +255 -162
- package/dist/src/mcp/oauth-token-storage.test.js.map +1 -1
- package/dist/src/mcp/token-storage/base-token-storage.d.ts +1 -1
- package/dist/src/mcp/token-storage/base-token-storage.js +1 -1
- package/dist/src/mcp/token-storage/base-token-storage.js.map +1 -1
- package/dist/src/mcp/token-storage/base-token-storage.test.js +1 -1
- package/dist/src/mcp/token-storage/base-token-storage.test.js.map +1 -1
- package/dist/src/mcp/token-storage/file-token-storage.d.ts +24 -0
- package/dist/src/mcp/token-storage/file-token-storage.js +144 -0
- package/dist/src/mcp/token-storage/file-token-storage.js.map +1 -0
- package/dist/src/mcp/token-storage/file-token-storage.test.d.ts +6 -0
- package/dist/src/mcp/token-storage/file-token-storage.test.js +235 -0
- package/dist/src/mcp/token-storage/file-token-storage.test.js.map +1 -0
- package/dist/src/mcp/token-storage/hybrid-token-storage.d.ts +23 -0
- package/dist/src/mcp/token-storage/hybrid-token-storage.js +78 -0
- package/dist/src/mcp/token-storage/hybrid-token-storage.js.map +1 -0
- package/dist/src/mcp/token-storage/hybrid-token-storage.test.d.ts +6 -0
- package/dist/src/mcp/token-storage/hybrid-token-storage.test.js +193 -0
- package/dist/src/mcp/token-storage/hybrid-token-storage.test.js.map +1 -0
- 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/mcp/token-storage/keychain-token-storage.d.ts +31 -0
- package/dist/src/mcp/token-storage/keychain-token-storage.js +190 -0
- package/dist/src/mcp/token-storage/keychain-token-storage.js.map +1 -0
- package/dist/src/mcp/token-storage/keychain-token-storage.test.d.ts +6 -0
- package/dist/src/mcp/token-storage/keychain-token-storage.test.js +254 -0
- package/dist/src/mcp/token-storage/keychain-token-storage.test.js.map +1 -0
- package/dist/src/mcp/token-storage/types.d.ts +4 -0
- package/dist/src/mcp/token-storage/types.js +5 -1
- package/dist/src/mcp/token-storage/types.js.map +1 -1
- 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 +92 -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 +515 -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 +7 -13
- package/dist/src/services/chatRecordingService.js +28 -19
- package/dist/src/services/chatRecordingService.js.map +1 -1
- package/dist/src/services/chatRecordingService.test.js +62 -20
- 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/activity-detector.d.ts +41 -0
- package/dist/src/telemetry/activity-detector.js +61 -0
- package/dist/src/telemetry/activity-detector.js.map +1 -0
- package/dist/src/telemetry/activity-detector.test.d.ts +6 -0
- package/dist/src/telemetry/activity-detector.test.js +136 -0
- package/dist/src/telemetry/activity-detector.test.js.map +1 -0
- package/dist/src/telemetry/activity-types.d.ts +19 -0
- package/dist/src/telemetry/activity-types.js +21 -0
- package/dist/src/telemetry/activity-types.js.map +1 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +16 -2
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +143 -24
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +101 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +19 -2
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +48 -2
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
- package/dist/src/telemetry/constants.d.ts +8 -0
- package/dist/src/telemetry/constants.js +8 -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/high-water-mark-tracker.d.ts +43 -0
- package/dist/src/telemetry/high-water-mark-tracker.js +88 -0
- package/dist/src/telemetry/high-water-mark-tracker.js.map +1 -0
- package/dist/src/telemetry/high-water-mark-tracker.test.d.ts +6 -0
- package/dist/src/telemetry/high-water-mark-tracker.test.js +152 -0
- package/dist/src/telemetry/high-water-mark-tracker.test.js.map +1 -0
- package/dist/src/telemetry/index.d.ts +7 -2
- package/dist/src/telemetry/index.js +7 -2
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/loggers.d.ts +8 -1
- package/dist/src/telemetry/loggers.js +140 -8
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/loggers.test.js +268 -39
- package/dist/src/telemetry/loggers.test.js.map +1 -1
- package/dist/src/telemetry/metrics.d.ts +4 -3
- package/dist/src/telemetry/metrics.js +33 -10
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/metrics.test.js +47 -25
- package/dist/src/telemetry/metrics.test.js.map +1 -1
- package/dist/src/telemetry/rate-limiter.d.ts +48 -0
- package/dist/src/telemetry/rate-limiter.js +100 -0
- package/dist/src/telemetry/rate-limiter.js.map +1 -0
- package/dist/src/telemetry/rate-limiter.test.d.ts +6 -0
- package/dist/src/telemetry/rate-limiter.test.js +207 -0
- package/dist/src/telemetry/rate-limiter.test.js.map +1 -0
- 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 +70 -6
- package/dist/src/telemetry/types.js +112 -8
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.d.ts +1 -1
- package/dist/src/telemetry/uiTelemetry.js +6 -7
- package/dist/src/telemetry/uiTelemetry.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.test.js +15 -15
- package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
- package/dist/src/test-utils/index.d.ts +6 -0
- package/dist/src/test-utils/index.js +7 -0
- package/dist/src/test-utils/index.js.map +1 -0
- package/dist/src/test-utils/mock-tool.d.ts +41 -0
- package/dist/src/test-utils/mock-tool.js +51 -0
- package/dist/src/test-utils/mock-tool.js.map +1 -0
- package/dist/src/tools/diffOptions.js +21 -13
- package/dist/src/tools/diffOptions.js.map +1 -1
- package/dist/src/tools/diffOptions.test.js +58 -22
- package/dist/src/tools/diffOptions.test.js.map +1 -1
- package/dist/src/tools/edit.d.ts +2 -2
- package/dist/src/tools/edit.js +35 -44
- package/dist/src/tools/edit.js.map +1 -1
- package/dist/src/tools/edit.test.js +124 -13
- 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/mcp-client-manager.d.ts +5 -3
- package/dist/src/tools/mcp-client-manager.js +13 -4
- package/dist/src/tools/mcp-client-manager.js.map +1 -1
- package/dist/src/tools/mcp-client-manager.test.js +20 -1
- package/dist/src/tools/mcp-client-manager.test.js.map +1 -1
- package/dist/src/tools/mcp-client.d.ts +5 -5
- package/dist/src/tools/mcp-client.js +40 -35
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/mcp-client.test.js +3 -3
- package/dist/src/tools/mcp-client.test.js.map +1 -1
- package/dist/src/tools/mcp-tool.d.ts +3 -2
- package/dist/src/tools/mcp-tool.js +9 -9
- package/dist/src/tools/mcp-tool.js.map +1 -1
- package/dist/src/tools/mcp-tool.test.js +28 -7
- package/dist/src/tools/mcp-tool.test.js.map +1 -1
- package/dist/src/tools/memoryTool.js +5 -33
- package/dist/src/tools/memoryTool.js.map +1 -1
- package/dist/src/tools/read-file.js +8 -3
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/read-file.test.js +29 -0
- package/dist/src/tools/read-file.test.js.map +1 -1
- package/dist/src/tools/read-many-files.d.ts +1 -1
- package/dist/src/tools/read-many-files.js +18 -50
- package/dist/src/tools/read-many-files.js.map +1 -1
- package/dist/src/tools/read-many-files.test.js +4 -4
- package/dist/src/tools/read-many-files.test.js.map +1 -1
- package/dist/src/tools/ripGrep.d.ts +8 -0
- package/dist/src/tools/ripGrep.js +26 -1
- package/dist/src/tools/ripGrep.js.map +1 -1
- package/dist/src/tools/ripGrep.test.js +107 -5
- 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 -24
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/shell.test.js +35 -70
- package/dist/src/tools/shell.test.js.map +1 -1
- package/dist/src/tools/smart-edit.d.ts +72 -0
- package/dist/src/tools/smart-edit.js +594 -0
- package/dist/src/tools/smart-edit.js.map +1 -0
- package/dist/src/tools/smart-edit.test.d.ts +6 -0
- package/dist/src/tools/smart-edit.test.js +419 -0
- package/dist/src/tools/smart-edit.test.js.map +1 -0
- package/dist/src/tools/tool-registry.d.ts +2 -1
- package/dist/src/tools/tool-registry.js +6 -5
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tools.d.ts +14 -7
- package/dist/src/tools/tools.js +9 -2
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/web-fetch.js +4 -3
- package/dist/src/tools/web-fetch.js.map +1 -1
- package/dist/src/tools/web-search.d.ts +1 -1
- package/dist/src/tools/web-search.js +3 -1
- package/dist/src/tools/web-search.js.map +1 -1
- package/dist/src/tools/write-file.js +14 -19
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/tools/write-file.test.js +99 -19
- 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.d.ts +20 -3
- package/dist/src/utils/fileUtils.js +154 -32
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/fileUtils.test.js +347 -29
- package/dist/src/utils/fileUtils.test.js.map +1 -1
- package/dist/src/utils/flashFallback.test.d.ts +6 -0
- package/dist/src/utils/{flashFallback.integration.test.js → flashFallback.test.js} +31 -27
- package/dist/src/utils/flashFallback.test.js.map +1 -0
- 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 -7
- package/dist/src/utils/gitIgnoreParser.js +125 -34
- package/dist/src/utils/gitIgnoreParser.js.map +1 -1
- package/dist/src/utils/gitIgnoreParser.test.js +66 -35
- package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
- package/dist/src/utils/llm-edit-fixer.d.ts +26 -0
- package/dist/src/utils/llm-edit-fixer.js +121 -0
- package/dist/src/utils/llm-edit-fixer.js.map +1 -0
- package/dist/src/utils/llm-edit-fixer.test.d.ts +6 -0
- package/dist/src/utils/llm-edit-fixer.test.js +105 -0
- package/dist/src/utils/llm-edit-fixer.test.js.map +1 -0
- package/dist/src/utils/memoryDiscovery.d.ts +5 -4
- package/dist/src/utils/memoryDiscovery.js +10 -9
- package/dist/src/utils/memoryDiscovery.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.test.js +50 -25
- 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 +75 -64
- package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
- package/dist/src/utils/promptIdContext.d.ts +7 -0
- package/dist/src/utils/promptIdContext.js +8 -0
- package/dist/src/utils/promptIdContext.js.map +1 -0
- 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 +9 -3
- package/dist/google-gemini-cli-core-0.3.0-preview.3.tgz +0 -0
- package/dist/src/utils/flashFallback.integration.test.js.map +0 -1
- /package/dist/src/{utils/flashFallback.integration.test.d.ts → code_assist/oauth-credential-storage.test.d.ts} +0 -0
|
@@ -4,25 +4,43 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
|
|
7
|
-
import {
|
|
8
|
-
import { findIndexAfterFraction, GeminiClient } from './client.js';
|
|
7
|
+
import { findCompressSplitPoint, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
|
|
9
8
|
import { AuthType, } from './contentGenerator.js';
|
|
10
9
|
import {} from './geminiChat.js';
|
|
11
|
-
import { Config } from '../config/config.js';
|
|
12
10
|
import { CompressionStatus, GeminiEventType, Turn, } from './turn.js';
|
|
13
11
|
import { getCoreSystemPrompt } from './prompts.js';
|
|
14
12
|
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
|
15
13
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
|
16
14
|
import { setSimulate429 } from '../utils/testUtils.js';
|
|
17
15
|
import { tokenLimit } from './tokenLimits.js';
|
|
18
|
-
import {
|
|
16
|
+
import { ideContextStore } from '../ide/ideContext.js';
|
|
19
17
|
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
|
18
|
+
import { uiTelemetryService } from '../telemetry/uiTelemetry.js';
|
|
19
|
+
// Mock fs module to prevent actual file system operations during tests
|
|
20
|
+
const mockFileSystem = new Map();
|
|
21
|
+
vi.mock('node:fs', () => {
|
|
22
|
+
const fsModule = {
|
|
23
|
+
mkdirSync: vi.fn(),
|
|
24
|
+
writeFileSync: vi.fn((path, data) => {
|
|
25
|
+
mockFileSystem.set(path, data);
|
|
26
|
+
}),
|
|
27
|
+
readFileSync: vi.fn((path) => {
|
|
28
|
+
if (mockFileSystem.has(path)) {
|
|
29
|
+
return mockFileSystem.get(path);
|
|
30
|
+
}
|
|
31
|
+
throw Object.assign(new Error('ENOENT: no such file or directory'), {
|
|
32
|
+
code: 'ENOENT',
|
|
33
|
+
});
|
|
34
|
+
}),
|
|
35
|
+
existsSync: vi.fn((path) => mockFileSystem.has(path)),
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
default: fsModule,
|
|
39
|
+
...fsModule,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
20
42
|
// --- Mocks ---
|
|
21
|
-
const mockChatCreateFn = vi.fn();
|
|
22
|
-
const mockGenerateContentFn = vi.fn();
|
|
23
|
-
const mockEmbedContentFn = vi.fn();
|
|
24
43
|
const mockTurnRunFn = vi.fn();
|
|
25
|
-
vi.mock('@google/genai');
|
|
26
44
|
vi.mock('./turn', async (importOriginal) => {
|
|
27
45
|
const actual = await importOriginal();
|
|
28
46
|
// Define a mock class that has the same shape as the real Turn
|
|
@@ -59,6 +77,11 @@ vi.mock('../telemetry/index.js', () => ({
|
|
|
59
77
|
logApiError: vi.fn(),
|
|
60
78
|
}));
|
|
61
79
|
vi.mock('../ide/ideContext.js');
|
|
80
|
+
vi.mock('../telemetry/uiTelemetry.js', () => ({
|
|
81
|
+
uiTelemetryService: {
|
|
82
|
+
setLastPromptTokenCount: vi.fn(),
|
|
83
|
+
},
|
|
84
|
+
}));
|
|
62
85
|
/**
|
|
63
86
|
* Array.fromAsync ponyfill, which will be available in es 2024.
|
|
64
87
|
*
|
|
@@ -72,41 +95,59 @@ async function fromAsync(promise) {
|
|
|
72
95
|
return results;
|
|
73
96
|
}
|
|
74
97
|
describe('findIndexAfterFraction', () => {
|
|
75
|
-
const history = [
|
|
76
|
-
{ role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66
|
|
77
|
-
{ role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68
|
|
78
|
-
{ role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66
|
|
79
|
-
{ role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68
|
|
80
|
-
{ role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65
|
|
81
|
-
];
|
|
82
|
-
// Total length: 333
|
|
83
98
|
it('should throw an error for non-positive numbers', () => {
|
|
84
|
-
expect(() =>
|
|
99
|
+
expect(() => findCompressSplitPoint([], 0)).toThrow('Fraction must be between 0 and 1');
|
|
85
100
|
});
|
|
86
101
|
it('should throw an error for a fraction greater than or equal to 1', () => {
|
|
87
|
-
expect(() =>
|
|
102
|
+
expect(() => findCompressSplitPoint([], 1)).toThrow('Fraction must be between 0 and 1');
|
|
103
|
+
});
|
|
104
|
+
it('should handle an empty history', () => {
|
|
105
|
+
expect(findCompressSplitPoint([], 0.5)).toBe(0);
|
|
88
106
|
});
|
|
89
107
|
it('should handle a fraction in the middle', () => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
const history = [
|
|
109
|
+
{ role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (19%)
|
|
110
|
+
{ role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (40%)
|
|
111
|
+
{ role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (60%)
|
|
112
|
+
{ role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (80%)
|
|
113
|
+
{ role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65 (100%)
|
|
114
|
+
];
|
|
115
|
+
expect(findCompressSplitPoint(history, 0.5)).toBe(2);
|
|
96
116
|
});
|
|
97
|
-
it('should handle a fraction
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
it('should handle a fraction of last index', () => {
|
|
118
|
+
const history = [
|
|
119
|
+
{ role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (19%)
|
|
120
|
+
{ role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (40%)
|
|
121
|
+
{ role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (60%)
|
|
122
|
+
{ role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (80%)
|
|
123
|
+
{ role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65 (100%)
|
|
124
|
+
];
|
|
125
|
+
expect(findCompressSplitPoint(history, 0.9)).toBe(4);
|
|
104
126
|
});
|
|
105
|
-
it('should handle
|
|
106
|
-
|
|
127
|
+
it('should handle a fraction of after last index', () => {
|
|
128
|
+
const history = [
|
|
129
|
+
{ role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (24%%)
|
|
130
|
+
{ role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (50%)
|
|
131
|
+
{ role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (74%)
|
|
132
|
+
{ role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (100%)
|
|
133
|
+
];
|
|
134
|
+
expect(findCompressSplitPoint(history, 0.8)).toBe(4);
|
|
135
|
+
});
|
|
136
|
+
it('should return earlier splitpoint if no valid ones are after threshhold', () => {
|
|
137
|
+
const history = [
|
|
138
|
+
{ role: 'user', parts: [{ text: 'This is the first message.' }] },
|
|
139
|
+
{ role: 'model', parts: [{ text: 'This is the second message.' }] },
|
|
140
|
+
{ role: 'user', parts: [{ text: 'This is the third message.' }] },
|
|
141
|
+
{ role: 'model', parts: [{ functionCall: {} }] },
|
|
142
|
+
];
|
|
143
|
+
// Can't return 4 because the previous item has a function call.
|
|
144
|
+
expect(findCompressSplitPoint(history, 0.99)).toBe(2);
|
|
107
145
|
});
|
|
108
146
|
it('should handle a history with only one item', () => {
|
|
109
|
-
|
|
147
|
+
const historyWithEmptyParts = [
|
|
148
|
+
{ role: 'user', parts: [{ text: 'Message 1' }] },
|
|
149
|
+
];
|
|
150
|
+
expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(0);
|
|
110
151
|
});
|
|
111
152
|
it('should handle history with weird parts', () => {
|
|
112
153
|
const historyWithEmptyParts = [
|
|
@@ -114,37 +155,55 @@ describe('findIndexAfterFraction', () => {
|
|
|
114
155
|
{ role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
|
|
115
156
|
{ role: 'user', parts: [{ text: 'Message 2' }] },
|
|
116
157
|
];
|
|
117
|
-
expect(
|
|
158
|
+
expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(2);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe('isThinkingSupported', () => {
|
|
162
|
+
it('should return true for gemini-2.5', () => {
|
|
163
|
+
expect(isThinkingSupported('gemini-2.5')).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
it('should return true for gemini-2.5-pro', () => {
|
|
166
|
+
expect(isThinkingSupported('gemini-2.5-pro')).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
it('should return false for other models', () => {
|
|
169
|
+
expect(isThinkingSupported('gemini-1.5-flash')).toBe(false);
|
|
170
|
+
expect(isThinkingSupported('some-other-model')).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('isThinkingDefault', () => {
|
|
174
|
+
it('should return false for gemini-2.5-flash-lite', () => {
|
|
175
|
+
expect(isThinkingDefault('gemini-2.5-flash-lite')).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
it('should return true for gemini-2.5', () => {
|
|
178
|
+
expect(isThinkingDefault('gemini-2.5')).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
it('should return true for gemini-2.5-pro', () => {
|
|
181
|
+
expect(isThinkingDefault('gemini-2.5-pro')).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
it('should return false for other models', () => {
|
|
184
|
+
expect(isThinkingDefault('gemini-1.5-flash')).toBe(false);
|
|
185
|
+
expect(isThinkingDefault('some-other-model')).toBe(false);
|
|
118
186
|
});
|
|
119
187
|
});
|
|
120
188
|
describe('Gemini Client (client.ts)', () => {
|
|
189
|
+
let mockContentGenerator;
|
|
190
|
+
let mockConfig;
|
|
121
191
|
let client;
|
|
192
|
+
let mockGenerateContentFn;
|
|
122
193
|
beforeEach(async () => {
|
|
123
194
|
vi.resetAllMocks();
|
|
195
|
+
vi.mocked(uiTelemetryService.setLastPromptTokenCount).mockClear();
|
|
196
|
+
mockGenerateContentFn = vi.fn().mockResolvedValue({
|
|
197
|
+
candidates: [{ content: { parts: [{ text: '{"key": "value"}' }] } }],
|
|
198
|
+
});
|
|
124
199
|
// Disable 429 simulation for tests
|
|
125
200
|
setSimulate429(false);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
generateContent: mockGenerateContentFn,
|
|
133
|
-
embedContent: mockEmbedContentFn,
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
return mock;
|
|
137
|
-
});
|
|
138
|
-
mockChatCreateFn.mockResolvedValue({});
|
|
139
|
-
mockGenerateContentFn.mockResolvedValue({
|
|
140
|
-
candidates: [
|
|
141
|
-
{
|
|
142
|
-
content: {
|
|
143
|
-
parts: [{ text: '{"key": "value"}' }],
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
],
|
|
147
|
-
});
|
|
201
|
+
mockContentGenerator = {
|
|
202
|
+
generateContent: mockGenerateContentFn,
|
|
203
|
+
generateContentStream: vi.fn(),
|
|
204
|
+
countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
|
|
205
|
+
batchEmbedContents: vi.fn(),
|
|
206
|
+
};
|
|
148
207
|
// Because the GeminiClient constructor kicks off an async process (startChat)
|
|
149
208
|
// that depends on a fully-formed Config object, we need to mock the
|
|
150
209
|
// entire implementation of Config for these tests.
|
|
@@ -154,12 +213,11 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
154
213
|
};
|
|
155
214
|
const fileService = new FileDiscoveryService('/test/dir');
|
|
156
215
|
const contentGeneratorConfig = {
|
|
157
|
-
model: 'test-model',
|
|
158
216
|
apiKey: 'test-key',
|
|
159
217
|
vertexai: false,
|
|
160
218
|
authType: AuthType.USE_GEMINI,
|
|
161
219
|
};
|
|
162
|
-
|
|
220
|
+
mockConfig = {
|
|
163
221
|
getContentGeneratorConfig: vi
|
|
164
222
|
.fn()
|
|
165
223
|
.mockReturnValue(contentGeneratorConfig),
|
|
@@ -187,159 +245,34 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
187
245
|
getDirectories: vi.fn().mockReturnValue(['/test/dir']),
|
|
188
246
|
}),
|
|
189
247
|
getGeminiClient: vi.fn(),
|
|
248
|
+
getModelRouterService: vi.fn().mockReturnValue({
|
|
249
|
+
route: vi.fn().mockResolvedValue({ model: 'default-routed-model' }),
|
|
250
|
+
}),
|
|
251
|
+
isInFallbackMode: vi.fn().mockReturnValue(false),
|
|
190
252
|
setFallbackMode: vi.fn(),
|
|
191
253
|
getChatCompression: vi.fn().mockReturnValue(undefined),
|
|
192
254
|
getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
|
|
255
|
+
getUseSmartEdit: vi.fn().mockReturnValue(false),
|
|
256
|
+
getUseModelRouter: vi.fn().mockReturnValue(false),
|
|
257
|
+
getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
|
|
258
|
+
storage: {
|
|
259
|
+
getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
|
|
260
|
+
},
|
|
261
|
+
getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
|
|
262
|
+
getBaseLlmClient: vi.fn().mockReturnValue({
|
|
263
|
+
generateJson: vi.fn().mockResolvedValue({
|
|
264
|
+
next_speaker: 'user',
|
|
265
|
+
reasoning: 'test',
|
|
266
|
+
}),
|
|
267
|
+
}),
|
|
193
268
|
};
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// and the constructor will use the mocked GoogleGenAI
|
|
198
|
-
client = new GeminiClient(new Config({ sessionId: 'test-session-id' }));
|
|
199
|
-
mockConfigObject.getGeminiClient.mockReturnValue(client);
|
|
200
|
-
await client.initialize(contentGeneratorConfig);
|
|
269
|
+
client = new GeminiClient(mockConfig);
|
|
270
|
+
await client.initialize();
|
|
271
|
+
vi.mocked(mockConfig.getGeminiClient).mockReturnValue(client);
|
|
201
272
|
});
|
|
202
273
|
afterEach(() => {
|
|
203
274
|
vi.restoreAllMocks();
|
|
204
275
|
});
|
|
205
|
-
// NOTE: The following tests for startChat were removed due to persistent issues with
|
|
206
|
-
// the @google/genai mock. Specifically, the mockChatCreateFn (representing instance.chats.create)
|
|
207
|
-
// was not being detected as called by the GeminiClient instance.
|
|
208
|
-
// This likely points to a subtle issue in how the GoogleGenerativeAI class constructor
|
|
209
|
-
// and its instance methods are mocked and then used by the class under test.
|
|
210
|
-
// For future debugging, ensure that the `this.client` in `GeminiClient` (which is an
|
|
211
|
-
// instance of the mocked GoogleGenerativeAI) correctly has its `chats.create` method
|
|
212
|
-
// pointing to `mockChatCreateFn`.
|
|
213
|
-
// it('startChat should call getCoreSystemPrompt with userMemory and pass to chats.create', async () => { ... });
|
|
214
|
-
// it('startChat should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
|
|
215
|
-
// NOTE: The following tests for generateJson were removed due to persistent issues with
|
|
216
|
-
// the @google/genai mock, similar to the startChat tests. The mockGenerateContentFn
|
|
217
|
-
// (representing instance.models.generateContent) was not being detected as called, or the mock
|
|
218
|
-
// was not preventing an actual API call (leading to API key errors).
|
|
219
|
-
// For future debugging, ensure `this.client.models.generateContent` in `GeminiClient` correctly
|
|
220
|
-
// uses the `mockGenerateContentFn`.
|
|
221
|
-
// it('generateJson should call getCoreSystemPrompt with userMemory and pass to generateContent', async () => { ... });
|
|
222
|
-
// it('generateJson should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
|
|
223
|
-
describe('generateEmbedding', () => {
|
|
224
|
-
const texts = ['hello world', 'goodbye world'];
|
|
225
|
-
const testEmbeddingModel = 'test-embedding-model';
|
|
226
|
-
it('should call embedContent with correct parameters and return embeddings', async () => {
|
|
227
|
-
const mockEmbeddings = [
|
|
228
|
-
[0.1, 0.2, 0.3],
|
|
229
|
-
[0.4, 0.5, 0.6],
|
|
230
|
-
];
|
|
231
|
-
const mockResponse = {
|
|
232
|
-
embeddings: [
|
|
233
|
-
{ values: mockEmbeddings[0] },
|
|
234
|
-
{ values: mockEmbeddings[1] },
|
|
235
|
-
],
|
|
236
|
-
};
|
|
237
|
-
mockEmbedContentFn.mockResolvedValue(mockResponse);
|
|
238
|
-
const result = await client.generateEmbedding(texts);
|
|
239
|
-
expect(mockEmbedContentFn).toHaveBeenCalledTimes(1);
|
|
240
|
-
expect(mockEmbedContentFn).toHaveBeenCalledWith({
|
|
241
|
-
model: testEmbeddingModel,
|
|
242
|
-
contents: texts,
|
|
243
|
-
});
|
|
244
|
-
expect(result).toEqual(mockEmbeddings);
|
|
245
|
-
});
|
|
246
|
-
it('should return an empty array if an empty array is passed', async () => {
|
|
247
|
-
const result = await client.generateEmbedding([]);
|
|
248
|
-
expect(result).toEqual([]);
|
|
249
|
-
expect(mockEmbedContentFn).not.toHaveBeenCalled();
|
|
250
|
-
});
|
|
251
|
-
it('should throw an error if API response has no embeddings array', async () => {
|
|
252
|
-
mockEmbedContentFn.mockResolvedValue({}); // No `embeddings` key
|
|
253
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
|
|
254
|
-
});
|
|
255
|
-
it('should throw an error if API response has an empty embeddings array', async () => {
|
|
256
|
-
const mockResponse = {
|
|
257
|
-
embeddings: [],
|
|
258
|
-
};
|
|
259
|
-
mockEmbedContentFn.mockResolvedValue(mockResponse);
|
|
260
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
|
|
261
|
-
});
|
|
262
|
-
it('should throw an error if API returns a mismatched number of embeddings', async () => {
|
|
263
|
-
const mockResponse = {
|
|
264
|
-
embeddings: [{ values: [1, 2, 3] }], // Only one for two texts
|
|
265
|
-
};
|
|
266
|
-
mockEmbedContentFn.mockResolvedValue(mockResponse);
|
|
267
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned a mismatched number of embeddings. Expected 2, got 1.');
|
|
268
|
-
});
|
|
269
|
-
it('should throw an error if any embedding has nullish values', async () => {
|
|
270
|
-
const mockResponse = {
|
|
271
|
-
embeddings: [{ values: [1, 2, 3] }, { values: undefined }], // Second one is bad
|
|
272
|
-
};
|
|
273
|
-
mockEmbedContentFn.mockResolvedValue(mockResponse);
|
|
274
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 1: "goodbye world"');
|
|
275
|
-
});
|
|
276
|
-
it('should throw an error if any embedding has an empty values array', async () => {
|
|
277
|
-
const mockResponse = {
|
|
278
|
-
embeddings: [{ values: [] }, { values: [1, 2, 3] }], // First one is bad
|
|
279
|
-
};
|
|
280
|
-
mockEmbedContentFn.mockResolvedValue(mockResponse);
|
|
281
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 0: "hello world"');
|
|
282
|
-
});
|
|
283
|
-
it('should propagate errors from the API call', async () => {
|
|
284
|
-
const apiError = new Error('API Failure');
|
|
285
|
-
mockEmbedContentFn.mockRejectedValue(apiError);
|
|
286
|
-
await expect(client.generateEmbedding(texts)).rejects.toThrow('API Failure');
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
describe('generateJson', () => {
|
|
290
|
-
it('should call generateContent with the correct parameters', async () => {
|
|
291
|
-
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
|
292
|
-
const schema = { type: 'string' };
|
|
293
|
-
const abortSignal = new AbortController().signal;
|
|
294
|
-
// Mock countTokens
|
|
295
|
-
const mockGenerator = {
|
|
296
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
|
|
297
|
-
generateContent: mockGenerateContentFn,
|
|
298
|
-
};
|
|
299
|
-
client['contentGenerator'] = mockGenerator;
|
|
300
|
-
await client.generateJson(contents, schema, abortSignal);
|
|
301
|
-
expect(mockGenerateContentFn).toHaveBeenCalledWith({
|
|
302
|
-
model: 'test-model', // Should use current model from config
|
|
303
|
-
config: {
|
|
304
|
-
abortSignal,
|
|
305
|
-
systemInstruction: getCoreSystemPrompt(''),
|
|
306
|
-
temperature: 0,
|
|
307
|
-
topP: 1,
|
|
308
|
-
responseJsonSchema: schema,
|
|
309
|
-
responseMimeType: 'application/json',
|
|
310
|
-
},
|
|
311
|
-
contents,
|
|
312
|
-
}, 'test-session-id');
|
|
313
|
-
});
|
|
314
|
-
it('should allow overriding model and config', async () => {
|
|
315
|
-
const contents = [
|
|
316
|
-
{ role: 'user', parts: [{ text: 'hello' }] },
|
|
317
|
-
];
|
|
318
|
-
const schema = { type: 'string' };
|
|
319
|
-
const abortSignal = new AbortController().signal;
|
|
320
|
-
const customModel = 'custom-json-model';
|
|
321
|
-
const customConfig = { temperature: 0.9, topK: 20 };
|
|
322
|
-
const mockGenerator = {
|
|
323
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
|
|
324
|
-
generateContent: mockGenerateContentFn,
|
|
325
|
-
};
|
|
326
|
-
client['contentGenerator'] = mockGenerator;
|
|
327
|
-
await client.generateJson(contents, schema, abortSignal, customModel, customConfig);
|
|
328
|
-
expect(mockGenerateContentFn).toHaveBeenCalledWith({
|
|
329
|
-
model: customModel,
|
|
330
|
-
config: {
|
|
331
|
-
abortSignal,
|
|
332
|
-
systemInstruction: getCoreSystemPrompt(''),
|
|
333
|
-
temperature: 0.9,
|
|
334
|
-
topP: 1, // from default
|
|
335
|
-
topK: 20,
|
|
336
|
-
responseJsonSchema: schema,
|
|
337
|
-
responseMimeType: 'application/json',
|
|
338
|
-
},
|
|
339
|
-
contents,
|
|
340
|
-
}, 'test-session-id');
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
276
|
describe('addHistory', () => {
|
|
344
277
|
it('should call chat.addHistory with the provided content', async () => {
|
|
345
278
|
const mockChat = {
|
|
@@ -377,21 +310,15 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
377
310
|
});
|
|
378
311
|
});
|
|
379
312
|
describe('tryCompressChat', () => {
|
|
380
|
-
const mockCountTokens = vi.fn();
|
|
381
|
-
const mockSendMessage = vi.fn();
|
|
382
313
|
const mockGetHistory = vi.fn();
|
|
383
314
|
beforeEach(() => {
|
|
384
315
|
vi.mock('./tokenLimits', () => ({
|
|
385
316
|
tokenLimit: vi.fn(),
|
|
386
317
|
}));
|
|
387
|
-
client['contentGenerator'] = {
|
|
388
|
-
countTokens: mockCountTokens,
|
|
389
|
-
};
|
|
390
318
|
client['chat'] = {
|
|
391
319
|
getHistory: mockGetHistory,
|
|
392
320
|
addHistory: vi.fn(),
|
|
393
321
|
setHistory: vi.fn(),
|
|
394
|
-
sendMessage: mockSendMessage,
|
|
395
322
|
};
|
|
396
323
|
});
|
|
397
324
|
function setup({ chatHistory = [
|
|
@@ -401,28 +328,21 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
401
328
|
const mockChat = {
|
|
402
329
|
getHistory: vi.fn().mockReturnValue(chatHistory),
|
|
403
330
|
setHistory: vi.fn(),
|
|
404
|
-
sendMessage: vi.fn().mockResolvedValue({ text: 'Summary' }),
|
|
405
331
|
};
|
|
406
|
-
|
|
407
|
-
.fn()
|
|
332
|
+
vi.mocked(mockContentGenerator.countTokens)
|
|
408
333
|
.mockResolvedValueOnce({ totalTokens: 1000 })
|
|
409
334
|
.mockResolvedValueOnce({ totalTokens: 5000 });
|
|
410
|
-
const mockGenerator = {
|
|
411
|
-
countTokens: mockCountTokens,
|
|
412
|
-
};
|
|
413
335
|
client['chat'] = mockChat;
|
|
414
|
-
client['contentGenerator'] = mockGenerator;
|
|
415
336
|
client['startChat'] = vi.fn().mockResolvedValue({ ...mockChat });
|
|
416
|
-
return { client, mockChat
|
|
337
|
+
return { client, mockChat };
|
|
417
338
|
}
|
|
418
339
|
describe('when compression inflates the token count', () => {
|
|
419
|
-
it('uses the truncated history for compression');
|
|
420
340
|
it('allows compression to be forced/manual after a failure', async () => {
|
|
421
|
-
const { client
|
|
422
|
-
|
|
341
|
+
const { client } = setup();
|
|
342
|
+
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
423
343
|
totalTokens: 1000,
|
|
424
344
|
});
|
|
425
|
-
await client.tryCompressChat('prompt-id-4'); // Fails
|
|
345
|
+
await client.tryCompressChat('prompt-id-4', false); // Fails
|
|
426
346
|
const result = await client.tryCompressChat('prompt-id-4', true);
|
|
427
347
|
expect(result).toEqual({
|
|
428
348
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
@@ -432,21 +352,26 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
432
352
|
});
|
|
433
353
|
it('yields the result even if the compression inflated the tokens', async () => {
|
|
434
354
|
const { client } = setup();
|
|
435
|
-
|
|
355
|
+
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
356
|
+
totalTokens: 1000,
|
|
357
|
+
});
|
|
358
|
+
const result = await client.tryCompressChat('prompt-id-4', false);
|
|
436
359
|
expect(result).toEqual({
|
|
437
360
|
compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
|
|
438
361
|
newTokenCount: 5000,
|
|
439
362
|
originalTokenCount: 1000,
|
|
440
363
|
});
|
|
364
|
+
expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(5000);
|
|
365
|
+
expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
|
|
441
366
|
});
|
|
442
367
|
it('does not manipulate the source chat', async () => {
|
|
443
368
|
const { client, mockChat } = setup();
|
|
444
|
-
await client.tryCompressChat('prompt-id-4',
|
|
369
|
+
await client.tryCompressChat('prompt-id-4', false);
|
|
445
370
|
expect(client['chat']).toBe(mockChat); // a new chat session was not created
|
|
446
371
|
});
|
|
447
372
|
it('restores the history back to the original', async () => {
|
|
448
373
|
vi.mocked(tokenLimit).mockReturnValue(1000);
|
|
449
|
-
|
|
374
|
+
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
450
375
|
totalTokens: 999,
|
|
451
376
|
});
|
|
452
377
|
const originalHistory = [
|
|
@@ -457,16 +382,16 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
457
382
|
const { client } = setup({
|
|
458
383
|
chatHistory: originalHistory,
|
|
459
384
|
});
|
|
460
|
-
const { compressionStatus } = await client.tryCompressChat('prompt-id-4');
|
|
385
|
+
const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false);
|
|
461
386
|
expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
|
|
462
387
|
expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
|
|
463
388
|
});
|
|
464
389
|
it('will not attempt to compress context after a failure', async () => {
|
|
465
|
-
const { client
|
|
466
|
-
await client.tryCompressChat('prompt-id-4');
|
|
467
|
-
const result = await client.tryCompressChat('prompt-id-5');
|
|
390
|
+
const { client } = setup();
|
|
391
|
+
await client.tryCompressChat('prompt-id-4', false);
|
|
392
|
+
const result = await client.tryCompressChat('prompt-id-5', false);
|
|
468
393
|
// it counts tokens for {original, compressed} and then never again
|
|
469
|
-
expect(
|
|
394
|
+
expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
|
|
470
395
|
expect(result).toEqual({
|
|
471
396
|
compressionStatus: CompressionStatus.NOOP,
|
|
472
397
|
newTokenCount: 0,
|
|
@@ -474,37 +399,17 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
474
399
|
});
|
|
475
400
|
});
|
|
476
401
|
});
|
|
477
|
-
it('attempts to compress with a maxOutputTokens set to the original token count', async () => {
|
|
478
|
-
vi.mocked(tokenLimit).mockReturnValue(1000);
|
|
479
|
-
mockCountTokens.mockResolvedValue({
|
|
480
|
-
totalTokens: 999,
|
|
481
|
-
});
|
|
482
|
-
mockGetHistory.mockReturnValue([
|
|
483
|
-
{ role: 'user', parts: [{ text: '...history...' }] },
|
|
484
|
-
]);
|
|
485
|
-
// Mock the summary response from the chat
|
|
486
|
-
mockSendMessage.mockResolvedValue({
|
|
487
|
-
role: 'model',
|
|
488
|
-
parts: [{ text: 'This is a summary.' }],
|
|
489
|
-
});
|
|
490
|
-
await client.tryCompressChat('prompt-id-2', true);
|
|
491
|
-
expect(mockSendMessage).toHaveBeenCalledWith(expect.objectContaining({
|
|
492
|
-
config: expect.objectContaining({
|
|
493
|
-
maxOutputTokens: 999,
|
|
494
|
-
}),
|
|
495
|
-
}), 'prompt-id-2');
|
|
496
|
-
});
|
|
497
402
|
it('should not trigger summarization if token count is below threshold', async () => {
|
|
498
403
|
const MOCKED_TOKEN_LIMIT = 1000;
|
|
499
404
|
vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
|
|
500
405
|
mockGetHistory.mockReturnValue([
|
|
501
406
|
{ role: 'user', parts: [{ text: '...history...' }] },
|
|
502
407
|
]);
|
|
503
|
-
|
|
408
|
+
vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
|
|
504
409
|
totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
|
|
505
410
|
});
|
|
506
411
|
const initialChat = client.getChat();
|
|
507
|
-
const result = await client.tryCompressChat('prompt-id-2');
|
|
412
|
+
const result = await client.tryCompressChat('prompt-id-2', false);
|
|
508
413
|
const newChat = client.getChat();
|
|
509
414
|
expect(tokenLimit).toHaveBeenCalled();
|
|
510
415
|
expect(result).toEqual({
|
|
@@ -527,19 +432,27 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
527
432
|
]);
|
|
528
433
|
const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
|
|
529
434
|
const newTokenCount = 100;
|
|
530
|
-
|
|
435
|
+
vi.mocked(mockContentGenerator.countTokens)
|
|
531
436
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
|
|
532
437
|
.mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
|
|
533
438
|
// Mock the summary response from the chat
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
439
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
440
|
+
candidates: [
|
|
441
|
+
{
|
|
442
|
+
content: {
|
|
443
|
+
role: 'model',
|
|
444
|
+
parts: [{ text: 'This is a summary.' }],
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
],
|
|
537
448
|
});
|
|
538
|
-
await client.tryCompressChat('prompt-id-3');
|
|
449
|
+
await client.tryCompressChat('prompt-id-3', false);
|
|
539
450
|
expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
|
|
540
451
|
tokens_before: originalTokenCount,
|
|
541
452
|
tokens_after: newTokenCount,
|
|
542
453
|
}));
|
|
454
|
+
expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(newTokenCount);
|
|
455
|
+
expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
|
|
543
456
|
});
|
|
544
457
|
it('should trigger summarization if token count is at threshold with contextPercentageThreshold setting', async () => {
|
|
545
458
|
const MOCKED_TOKEN_LIMIT = 1000;
|
|
@@ -553,19 +466,25 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
553
466
|
]);
|
|
554
467
|
const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
|
|
555
468
|
const newTokenCount = 100;
|
|
556
|
-
|
|
469
|
+
vi.mocked(mockContentGenerator.countTokens)
|
|
557
470
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
|
|
558
471
|
.mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
|
|
559
472
|
// Mock the summary response from the chat
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
473
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
474
|
+
candidates: [
|
|
475
|
+
{
|
|
476
|
+
content: {
|
|
477
|
+
role: 'model',
|
|
478
|
+
parts: [{ text: 'This is a summary.' }],
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
],
|
|
563
482
|
});
|
|
564
483
|
const initialChat = client.getChat();
|
|
565
|
-
const result = await client.tryCompressChat('prompt-id-3');
|
|
484
|
+
const result = await client.tryCompressChat('prompt-id-3', false);
|
|
566
485
|
const newChat = client.getChat();
|
|
567
486
|
expect(tokenLimit).toHaveBeenCalled();
|
|
568
|
-
expect(
|
|
487
|
+
expect(mockGenerateContentFn).toHaveBeenCalled();
|
|
569
488
|
// Assert that summarization happened and returned the correct stats
|
|
570
489
|
expect(result).toEqual({
|
|
571
490
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
@@ -598,19 +517,25 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
598
517
|
]);
|
|
599
518
|
const originalTokenCount = 1000 * 0.7;
|
|
600
519
|
const newTokenCount = 100;
|
|
601
|
-
|
|
520
|
+
vi.mocked(mockContentGenerator.countTokens)
|
|
602
521
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
|
|
603
522
|
.mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
|
|
604
523
|
// Mock the summary response from the chat
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
524
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
525
|
+
candidates: [
|
|
526
|
+
{
|
|
527
|
+
content: {
|
|
528
|
+
role: 'model',
|
|
529
|
+
parts: [{ text: 'This is a summary.' }],
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
],
|
|
608
533
|
});
|
|
609
534
|
const initialChat = client.getChat();
|
|
610
|
-
const result = await client.tryCompressChat('prompt-id-3');
|
|
535
|
+
const result = await client.tryCompressChat('prompt-id-3', false);
|
|
611
536
|
const newChat = client.getChat();
|
|
612
537
|
expect(tokenLimit).toHaveBeenCalled();
|
|
613
|
-
expect(
|
|
538
|
+
expect(mockGenerateContentFn).toHaveBeenCalled();
|
|
614
539
|
// Assert that summarization happened and returned the correct stats
|
|
615
540
|
expect(result).toEqual({
|
|
616
541
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
@@ -632,18 +557,24 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
632
557
|
]);
|
|
633
558
|
const originalTokenCount = 10; // Well below threshold
|
|
634
559
|
const newTokenCount = 5;
|
|
635
|
-
|
|
560
|
+
vi.mocked(mockContentGenerator.countTokens)
|
|
636
561
|
.mockResolvedValueOnce({ totalTokens: originalTokenCount })
|
|
637
562
|
.mockResolvedValueOnce({ totalTokens: newTokenCount });
|
|
638
563
|
// Mock the summary response from the chat
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
564
|
+
mockGenerateContentFn.mockResolvedValue({
|
|
565
|
+
candidates: [
|
|
566
|
+
{
|
|
567
|
+
content: {
|
|
568
|
+
role: 'model',
|
|
569
|
+
parts: [{ text: 'This is a summary.' }],
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
],
|
|
642
573
|
});
|
|
643
574
|
const initialChat = client.getChat();
|
|
644
|
-
const result = await client.tryCompressChat('prompt-id-1',
|
|
575
|
+
const result = await client.tryCompressChat('prompt-id-1', false); // force = true
|
|
645
576
|
const newChat = client.getChat();
|
|
646
|
-
expect(
|
|
577
|
+
expect(mockGenerateContentFn).toHaveBeenCalled();
|
|
647
578
|
expect(result).toEqual({
|
|
648
579
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
649
580
|
originalTokenCount,
|
|
@@ -653,9 +584,14 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
653
584
|
expect(newChat).not.toBe(initialChat);
|
|
654
585
|
});
|
|
655
586
|
it('should use current model from config for token counting after sendMessage', async () => {
|
|
656
|
-
const initialModel =
|
|
657
|
-
|
|
658
|
-
|
|
587
|
+
const initialModel = mockConfig.getModel();
|
|
588
|
+
// mock the model has been changed between calls of `countTokens`
|
|
589
|
+
const firstCurrentModel = initialModel + '-changed-1';
|
|
590
|
+
const secondCurrentModel = initialModel + '-changed-2';
|
|
591
|
+
vi.mocked(mockConfig.getModel)
|
|
592
|
+
.mockReturnValueOnce(firstCurrentModel)
|
|
593
|
+
.mockReturnValueOnce(secondCurrentModel);
|
|
594
|
+
vi.mocked(mockContentGenerator.countTokens)
|
|
659
595
|
.mockResolvedValueOnce({ totalTokens: 100000 })
|
|
660
596
|
.mockResolvedValueOnce({ totalTokens: 5000 });
|
|
661
597
|
const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
|
|
@@ -664,29 +600,19 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
664
600
|
{ role: 'model', parts: [{ text: 'Long response' }] },
|
|
665
601
|
];
|
|
666
602
|
const mockChat = {
|
|
667
|
-
getHistory: vi.fn().
|
|
603
|
+
getHistory: vi.fn().mockImplementation(() => [...mockChatHistory]),
|
|
668
604
|
setHistory: vi.fn(),
|
|
669
605
|
sendMessage: mockSendMessage,
|
|
670
606
|
};
|
|
671
|
-
const mockGenerator = {
|
|
672
|
-
countTokens: mockCountTokens,
|
|
673
|
-
};
|
|
674
|
-
// mock the model has been changed between calls of `countTokens`
|
|
675
|
-
const firstCurrentModel = initialModel + '-changed-1';
|
|
676
|
-
const secondCurrentModel = initialModel + '-changed-2';
|
|
677
|
-
vi.spyOn(client['config'], 'getModel')
|
|
678
|
-
.mockReturnValueOnce(firstCurrentModel)
|
|
679
|
-
.mockReturnValueOnce(secondCurrentModel);
|
|
680
607
|
client['chat'] = mockChat;
|
|
681
|
-
client['contentGenerator'] = mockGenerator;
|
|
682
608
|
client['startChat'] = vi.fn().mockResolvedValue(mockChat);
|
|
683
|
-
const result = await client.tryCompressChat('prompt-id-4',
|
|
684
|
-
expect(
|
|
685
|
-
expect(
|
|
609
|
+
const result = await client.tryCompressChat('prompt-id-4', false);
|
|
610
|
+
expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
|
|
611
|
+
expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(1, {
|
|
686
612
|
model: firstCurrentModel,
|
|
687
|
-
contents: mockChatHistory,
|
|
613
|
+
contents: [...mockChatHistory],
|
|
688
614
|
});
|
|
689
|
-
expect(
|
|
615
|
+
expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(2, {
|
|
690
616
|
model: secondCurrentModel,
|
|
691
617
|
contents: expect.any(Array),
|
|
692
618
|
});
|
|
@@ -700,20 +626,9 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
700
626
|
describe('sendMessageStream', () => {
|
|
701
627
|
it('emits a compression event when the context was automatically compressed', async () => {
|
|
702
628
|
// Arrange
|
|
703
|
-
|
|
629
|
+
mockTurnRunFn.mockReturnValue((async function* () {
|
|
704
630
|
yield { type: 'content', value: 'Hello' };
|
|
705
|
-
})();
|
|
706
|
-
mockTurnRunFn.mockReturnValue(mockStream);
|
|
707
|
-
const mockChat = {
|
|
708
|
-
addHistory: vi.fn(),
|
|
709
|
-
getHistory: vi.fn().mockReturnValue([]),
|
|
710
|
-
};
|
|
711
|
-
client['chat'] = mockChat;
|
|
712
|
-
const mockGenerator = {
|
|
713
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
714
|
-
generateContent: mockGenerateContentFn,
|
|
715
|
-
};
|
|
716
|
-
client['contentGenerator'] = mockGenerator;
|
|
631
|
+
})());
|
|
717
632
|
const compressionInfo = {
|
|
718
633
|
compressionStatus: CompressionStatus.COMPRESSED,
|
|
719
634
|
originalTokenCount: 1000,
|
|
@@ -743,16 +658,6 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
743
658
|
yield { type: 'content', value: 'Hello' };
|
|
744
659
|
})();
|
|
745
660
|
mockTurnRunFn.mockReturnValue(mockStream);
|
|
746
|
-
const mockChat = {
|
|
747
|
-
addHistory: vi.fn(),
|
|
748
|
-
getHistory: vi.fn().mockReturnValue([]),
|
|
749
|
-
};
|
|
750
|
-
client['chat'] = mockChat;
|
|
751
|
-
const mockGenerator = {
|
|
752
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
753
|
-
generateContent: mockGenerateContentFn,
|
|
754
|
-
};
|
|
755
|
-
client['contentGenerator'] = mockGenerator;
|
|
756
661
|
const compressionInfo = {
|
|
757
662
|
compressionStatus,
|
|
758
663
|
originalTokenCount: 1000,
|
|
@@ -770,7 +675,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
770
675
|
});
|
|
771
676
|
it('should include editor context when ideMode is enabled', async () => {
|
|
772
677
|
// Arrange
|
|
773
|
-
vi.mocked(
|
|
678
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
774
679
|
workspaceState: {
|
|
775
680
|
openFiles: [
|
|
776
681
|
{
|
|
@@ -791,21 +696,20 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
791
696
|
],
|
|
792
697
|
},
|
|
793
698
|
});
|
|
794
|
-
vi.
|
|
795
|
-
|
|
699
|
+
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
|
|
700
|
+
vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
|
|
701
|
+
originalTokenCount: 0,
|
|
702
|
+
newTokenCount: 0,
|
|
703
|
+
compressionStatus: CompressionStatus.COMPRESSED,
|
|
704
|
+
});
|
|
705
|
+
mockTurnRunFn.mockReturnValue((async function* () {
|
|
796
706
|
yield { type: 'content', value: 'Hello' };
|
|
797
|
-
})();
|
|
798
|
-
mockTurnRunFn.mockReturnValue(mockStream);
|
|
707
|
+
})());
|
|
799
708
|
const mockChat = {
|
|
800
709
|
addHistory: vi.fn(),
|
|
801
710
|
getHistory: vi.fn().mockReturnValue([]),
|
|
802
711
|
};
|
|
803
712
|
client['chat'] = mockChat;
|
|
804
|
-
const mockGenerator = {
|
|
805
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
806
|
-
generateContent: mockGenerateContentFn,
|
|
807
|
-
};
|
|
808
|
-
client['contentGenerator'] = mockGenerator;
|
|
809
713
|
const initialRequest = [{ text: 'Hi' }];
|
|
810
714
|
// Act
|
|
811
715
|
const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
|
|
@@ -813,7 +717,7 @@ describe('Gemini Client (client.ts)', () => {
|
|
|
813
717
|
// consume stream
|
|
814
718
|
}
|
|
815
719
|
// Assert
|
|
816
|
-
expect(
|
|
720
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
817
721
|
const expectedContext = `
|
|
818
722
|
Here is the user's editor context as a JSON object. This is for your information only.
|
|
819
723
|
\`\`\`json
|
|
@@ -838,7 +742,7 @@ ${JSON.stringify({
|
|
|
838
742
|
});
|
|
839
743
|
it('should not add context if ideMode is enabled but no open files', async () => {
|
|
840
744
|
// Arrange
|
|
841
|
-
vi.mocked(
|
|
745
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
842
746
|
workspaceState: {
|
|
843
747
|
openFiles: [],
|
|
844
748
|
},
|
|
@@ -853,11 +757,6 @@ ${JSON.stringify({
|
|
|
853
757
|
getHistory: vi.fn().mockReturnValue([]),
|
|
854
758
|
};
|
|
855
759
|
client['chat'] = mockChat;
|
|
856
|
-
const mockGenerator = {
|
|
857
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
858
|
-
generateContent: mockGenerateContentFn,
|
|
859
|
-
};
|
|
860
|
-
client['contentGenerator'] = mockGenerator;
|
|
861
760
|
const initialRequest = [{ text: 'Hi' }];
|
|
862
761
|
// Act
|
|
863
762
|
const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
|
|
@@ -865,12 +764,16 @@ ${JSON.stringify({
|
|
|
865
764
|
// consume stream
|
|
866
765
|
}
|
|
867
766
|
// Assert
|
|
868
|
-
expect(
|
|
869
|
-
|
|
767
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
768
|
+
// The `turn.run` method is now called with the model name as the first
|
|
769
|
+
// argument. We use `expect.any(String)` because this test is
|
|
770
|
+
// concerned with the IDE context logic, not the model routing,
|
|
771
|
+
// which is tested in its own dedicated suite.
|
|
772
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith(expect.any(String), initialRequest, expect.any(Object));
|
|
870
773
|
});
|
|
871
774
|
it('should add context if ideMode is enabled and there is one active file', async () => {
|
|
872
775
|
// Arrange
|
|
873
|
-
vi.mocked(
|
|
776
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
874
777
|
workspaceState: {
|
|
875
778
|
openFiles: [
|
|
876
779
|
{
|
|
@@ -884,6 +787,11 @@ ${JSON.stringify({
|
|
|
884
787
|
},
|
|
885
788
|
});
|
|
886
789
|
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
|
|
790
|
+
vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
|
|
791
|
+
originalTokenCount: 0,
|
|
792
|
+
newTokenCount: 0,
|
|
793
|
+
compressionStatus: CompressionStatus.COMPRESSED,
|
|
794
|
+
});
|
|
887
795
|
const mockStream = (async function* () {
|
|
888
796
|
yield { type: 'content', value: 'Hello' };
|
|
889
797
|
})();
|
|
@@ -893,11 +801,6 @@ ${JSON.stringify({
|
|
|
893
801
|
getHistory: vi.fn().mockReturnValue([]),
|
|
894
802
|
};
|
|
895
803
|
client['chat'] = mockChat;
|
|
896
|
-
const mockGenerator = {
|
|
897
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
898
|
-
generateContent: mockGenerateContentFn,
|
|
899
|
-
};
|
|
900
|
-
client['contentGenerator'] = mockGenerator;
|
|
901
804
|
const initialRequest = [{ text: 'Hi' }];
|
|
902
805
|
// Act
|
|
903
806
|
const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
|
|
@@ -905,7 +808,7 @@ ${JSON.stringify({
|
|
|
905
808
|
// consume stream
|
|
906
809
|
}
|
|
907
810
|
// Assert
|
|
908
|
-
expect(
|
|
811
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
909
812
|
const expectedContext = `
|
|
910
813
|
Here is the user's editor context as a JSON object. This is for your information only.
|
|
911
814
|
\`\`\`json
|
|
@@ -929,7 +832,7 @@ ${JSON.stringify({
|
|
|
929
832
|
});
|
|
930
833
|
it('should add context if ideMode is enabled and there are open files but no active file', async () => {
|
|
931
834
|
// Arrange
|
|
932
|
-
vi.mocked(
|
|
835
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
933
836
|
workspaceState: {
|
|
934
837
|
openFiles: [
|
|
935
838
|
{
|
|
@@ -944,6 +847,11 @@ ${JSON.stringify({
|
|
|
944
847
|
},
|
|
945
848
|
});
|
|
946
849
|
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
|
|
850
|
+
vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
|
|
851
|
+
originalTokenCount: 0,
|
|
852
|
+
newTokenCount: 0,
|
|
853
|
+
compressionStatus: CompressionStatus.COMPRESSED,
|
|
854
|
+
});
|
|
947
855
|
const mockStream = (async function* () {
|
|
948
856
|
yield { type: 'content', value: 'Hello' };
|
|
949
857
|
})();
|
|
@@ -953,11 +861,6 @@ ${JSON.stringify({
|
|
|
953
861
|
getHistory: vi.fn().mockReturnValue([]),
|
|
954
862
|
};
|
|
955
863
|
client['chat'] = mockChat;
|
|
956
|
-
const mockGenerator = {
|
|
957
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
958
|
-
generateContent: mockGenerateContentFn,
|
|
959
|
-
};
|
|
960
|
-
client['contentGenerator'] = mockGenerator;
|
|
961
864
|
const initialRequest = [{ text: 'Hi' }];
|
|
962
865
|
// Act
|
|
963
866
|
const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
|
|
@@ -965,7 +868,7 @@ ${JSON.stringify({
|
|
|
965
868
|
// consume stream
|
|
966
869
|
}
|
|
967
870
|
// Assert
|
|
968
|
-
expect(
|
|
871
|
+
expect(ideContextStore.get).toHaveBeenCalled();
|
|
969
872
|
const expectedContext = `
|
|
970
873
|
Here is the user's editor context as a JSON object. This is for your information only.
|
|
971
874
|
\`\`\`json
|
|
@@ -991,11 +894,6 @@ ${JSON.stringify({
|
|
|
991
894
|
getHistory: vi.fn().mockReturnValue([]),
|
|
992
895
|
};
|
|
993
896
|
client['chat'] = mockChat;
|
|
994
|
-
const mockGenerator = {
|
|
995
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
996
|
-
generateContent: mockGenerateContentFn,
|
|
997
|
-
};
|
|
998
|
-
client['contentGenerator'] = mockGenerator;
|
|
999
897
|
// Act
|
|
1000
898
|
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
|
|
1001
899
|
// Consume the stream manually to get the final return value.
|
|
@@ -1028,11 +926,6 @@ ${JSON.stringify({
|
|
|
1028
926
|
getHistory: vi.fn().mockReturnValue([]),
|
|
1029
927
|
};
|
|
1030
928
|
client['chat'] = mockChat;
|
|
1031
|
-
const mockGenerator = {
|
|
1032
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
1033
|
-
generateContent: mockGenerateContentFn,
|
|
1034
|
-
};
|
|
1035
|
-
client['contentGenerator'] = mockGenerator;
|
|
1036
929
|
// Use a signal that never gets aborted
|
|
1037
930
|
const abortController = new AbortController();
|
|
1038
931
|
const signal = abortController.signal;
|
|
@@ -1095,11 +988,6 @@ ${JSON.stringify({
|
|
|
1095
988
|
getHistory: vi.fn().mockReturnValue([]),
|
|
1096
989
|
};
|
|
1097
990
|
client['chat'] = mockChat;
|
|
1098
|
-
const mockGenerator = {
|
|
1099
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
1100
|
-
generateContent: mockGenerateContentFn,
|
|
1101
|
-
};
|
|
1102
|
-
client['contentGenerator'] = mockGenerator;
|
|
1103
991
|
// Act & Assert
|
|
1104
992
|
// Run up to the limit
|
|
1105
993
|
for (let i = 0; i < MAX_SESSION_TURNS; i++) {
|
|
@@ -1138,11 +1026,6 @@ ${JSON.stringify({
|
|
|
1138
1026
|
getHistory: vi.fn().mockReturnValue([]),
|
|
1139
1027
|
};
|
|
1140
1028
|
client['chat'] = mockChat;
|
|
1141
|
-
const mockGenerator = {
|
|
1142
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
1143
|
-
generateContent: mockGenerateContentFn,
|
|
1144
|
-
};
|
|
1145
|
-
client['contentGenerator'] = mockGenerator;
|
|
1146
1029
|
// Use a signal that never gets aborted
|
|
1147
1030
|
const abortController = new AbortController();
|
|
1148
1031
|
const signal = abortController.signal;
|
|
@@ -1181,6 +1064,91 @@ ${JSON.stringify({
|
|
|
1181
1064
|
console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
|
|
1182
1065
|
`${eventCount} events generated (properly bounded by MAX_TURNS)`);
|
|
1183
1066
|
});
|
|
1067
|
+
describe('Model Routing', () => {
|
|
1068
|
+
let mockRouterService;
|
|
1069
|
+
beforeEach(() => {
|
|
1070
|
+
mockRouterService = {
|
|
1071
|
+
route: vi
|
|
1072
|
+
.fn()
|
|
1073
|
+
.mockResolvedValue({ model: 'routed-model', reason: 'test' }),
|
|
1074
|
+
};
|
|
1075
|
+
vi.mocked(mockConfig.getModelRouterService).mockReturnValue(mockRouterService);
|
|
1076
|
+
mockTurnRunFn.mockReturnValue((async function* () {
|
|
1077
|
+
yield { type: 'content', value: 'Hello' };
|
|
1078
|
+
})());
|
|
1079
|
+
});
|
|
1080
|
+
it('should use the model router service to select a model on the first turn', async () => {
|
|
1081
|
+
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1082
|
+
await fromAsync(stream); // consume stream
|
|
1083
|
+
expect(mockConfig.getModelRouterService).toHaveBeenCalled();
|
|
1084
|
+
expect(mockRouterService.route).toHaveBeenCalled();
|
|
1085
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', // The model from the router
|
|
1086
|
+
[{ text: 'Hi' }], expect.any(Object));
|
|
1087
|
+
});
|
|
1088
|
+
it('should use the same model for subsequent turns in the same prompt (stickiness)', async () => {
|
|
1089
|
+
// First turn
|
|
1090
|
+
let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1091
|
+
await fromAsync(stream);
|
|
1092
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
|
1093
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
|
|
1094
|
+
// Second turn
|
|
1095
|
+
stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-1');
|
|
1096
|
+
await fromAsync(stream);
|
|
1097
|
+
// Router should not be called again
|
|
1098
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
|
1099
|
+
// Should stick to the first model
|
|
1100
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Continue' }], expect.any(Object));
|
|
1101
|
+
});
|
|
1102
|
+
it('should reset the sticky model and re-route when the prompt_id changes', async () => {
|
|
1103
|
+
// First prompt
|
|
1104
|
+
let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1105
|
+
await fromAsync(stream);
|
|
1106
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
|
1107
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
|
|
1108
|
+
// New prompt
|
|
1109
|
+
mockRouterService.route.mockResolvedValue({
|
|
1110
|
+
model: 'new-routed-model',
|
|
1111
|
+
reason: 'test',
|
|
1112
|
+
});
|
|
1113
|
+
stream = client.sendMessageStream([{ text: 'A new topic' }], new AbortController().signal, 'prompt-2');
|
|
1114
|
+
await fromAsync(stream);
|
|
1115
|
+
// Router should be called again for the new prompt
|
|
1116
|
+
expect(mockRouterService.route).toHaveBeenCalledTimes(2);
|
|
1117
|
+
// Should use the newly routed model
|
|
1118
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith('new-routed-model', [{ text: 'A new topic' }], expect.any(Object));
|
|
1119
|
+
});
|
|
1120
|
+
it('should use the fallback model and bypass routing when in fallback mode', async () => {
|
|
1121
|
+
vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
|
|
1122
|
+
mockRouterService.route.mockResolvedValue({
|
|
1123
|
+
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
1124
|
+
reason: 'fallback',
|
|
1125
|
+
});
|
|
1126
|
+
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
|
|
1127
|
+
await fromAsync(stream);
|
|
1128
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
|
|
1129
|
+
});
|
|
1130
|
+
it('should stick to the fallback model for the entire sequence even if fallback mode ends', async () => {
|
|
1131
|
+
// Start the sequence in fallback mode
|
|
1132
|
+
vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
|
|
1133
|
+
mockRouterService.route.mockResolvedValue({
|
|
1134
|
+
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
1135
|
+
reason: 'fallback',
|
|
1136
|
+
});
|
|
1137
|
+
let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-fallback-stickiness');
|
|
1138
|
+
await fromAsync(stream);
|
|
1139
|
+
// First call should use fallback model
|
|
1140
|
+
expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
|
|
1141
|
+
// End fallback mode
|
|
1142
|
+
vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(false);
|
|
1143
|
+
// Second call in the same sequence
|
|
1144
|
+
stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-fallback-stickiness');
|
|
1145
|
+
await fromAsync(stream);
|
|
1146
|
+
// Router should still not be called, and it should stick to the fallback model
|
|
1147
|
+
expect(mockTurnRunFn).toHaveBeenCalledTimes(2); // Ensure it was called again
|
|
1148
|
+
expect(mockTurnRunFn).toHaveBeenLastCalledWith(DEFAULT_GEMINI_FLASH_MODEL, // Still the fallback model
|
|
1149
|
+
[{ text: 'Continue' }], expect.any(Object));
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1184
1152
|
describe('Editor context delta', () => {
|
|
1185
1153
|
const mockStream = (async function* () {
|
|
1186
1154
|
yield { type: 'content', value: 'Hello' };
|
|
@@ -1197,7 +1165,6 @@ ${JSON.stringify({
|
|
|
1197
1165
|
const mockChat = {
|
|
1198
1166
|
addHistory: vi.fn(),
|
|
1199
1167
|
setHistory: vi.fn(),
|
|
1200
|
-
sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
|
|
1201
1168
|
// Assume history is not empty for delta checks
|
|
1202
1169
|
getHistory: vi
|
|
1203
1170
|
.fn()
|
|
@@ -1206,11 +1173,6 @@ ${JSON.stringify({
|
|
|
1206
1173
|
]),
|
|
1207
1174
|
};
|
|
1208
1175
|
client['chat'] = mockChat;
|
|
1209
|
-
const mockGenerator = {
|
|
1210
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
1211
|
-
generateContent: mockGenerateContentFn,
|
|
1212
|
-
};
|
|
1213
|
-
client['contentGenerator'] = mockGenerator;
|
|
1214
1176
|
});
|
|
1215
1177
|
const testCases = [
|
|
1216
1178
|
{
|
|
@@ -1326,7 +1288,7 @@ ${JSON.stringify({
|
|
|
1326
1288
|
},
|
|
1327
1289
|
};
|
|
1328
1290
|
// Setup current context
|
|
1329
|
-
vi.mocked(
|
|
1291
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1330
1292
|
workspaceState: {
|
|
1331
1293
|
openFiles: [
|
|
1332
1294
|
{ ...currentActiveFile, isActive: true, timestamp: Date.now() },
|
|
@@ -1372,7 +1334,7 @@ ${JSON.stringify({
|
|
|
1372
1334
|
},
|
|
1373
1335
|
};
|
|
1374
1336
|
// Setup current context (same as previous)
|
|
1375
|
-
vi.mocked(
|
|
1337
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1376
1338
|
workspaceState: {
|
|
1377
1339
|
openFiles: [
|
|
1378
1340
|
{ ...activeFile, isActive: true, timestamp: Date.now() },
|
|
@@ -1417,15 +1379,10 @@ ${JSON.stringify({
|
|
|
1417
1379
|
addHistory: vi.fn(),
|
|
1418
1380
|
getHistory: vi.fn().mockReturnValue([]), // Default empty history
|
|
1419
1381
|
setHistory: vi.fn(),
|
|
1420
|
-
sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
|
|
1421
1382
|
};
|
|
1422
1383
|
client['chat'] = mockChat;
|
|
1423
|
-
const mockGenerator = {
|
|
1424
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
1425
|
-
};
|
|
1426
|
-
client['contentGenerator'] = mockGenerator;
|
|
1427
1384
|
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
|
|
1428
|
-
vi.mocked(
|
|
1385
|
+
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1429
1386
|
workspaceState: {
|
|
1430
1387
|
openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
|
|
1431
1388
|
},
|
|
@@ -1501,7 +1458,7 @@ ${JSON.stringify({
|
|
|
1501
1458
|
openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
|
|
1502
1459
|
},
|
|
1503
1460
|
};
|
|
1504
|
-
vi.mocked(
|
|
1461
|
+
vi.mocked(ideContextStore.get).mockReturnValue(initialIdeContext);
|
|
1505
1462
|
// Act: Send the tool response
|
|
1506
1463
|
let stream = client.sendMessageStream([
|
|
1507
1464
|
{
|
|
@@ -1547,7 +1504,7 @@ ${JSON.stringify({
|
|
|
1547
1504
|
openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
|
|
1548
1505
|
},
|
|
1549
1506
|
};
|
|
1550
|
-
vi.mocked(
|
|
1507
|
+
vi.mocked(ideContextStore.get).mockReturnValue(newIdeContext);
|
|
1551
1508
|
// Act: Send a new, regular user message
|
|
1552
1509
|
stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
|
|
1553
1510
|
for await (const _ of stream) {
|
|
@@ -1577,7 +1534,7 @@ ${JSON.stringify({
|
|
|
1577
1534
|
],
|
|
1578
1535
|
},
|
|
1579
1536
|
};
|
|
1580
|
-
vi.mocked(
|
|
1537
|
+
vi.mocked(ideContextStore.get).mockReturnValue(contextA);
|
|
1581
1538
|
// Act: Send a regular message to establish the initial context
|
|
1582
1539
|
let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
|
|
1583
1540
|
for await (const _ of stream) {
|
|
@@ -1610,7 +1567,7 @@ ${JSON.stringify({
|
|
|
1610
1567
|
],
|
|
1611
1568
|
},
|
|
1612
1569
|
};
|
|
1613
|
-
vi.mocked(
|
|
1570
|
+
vi.mocked(ideContextStore.get).mockReturnValue(contextB);
|
|
1614
1571
|
// Act: Send the tool response
|
|
1615
1572
|
stream = client.sendMessageStream([
|
|
1616
1573
|
{
|
|
@@ -1655,7 +1612,7 @@ ${JSON.stringify({
|
|
|
1655
1612
|
],
|
|
1656
1613
|
},
|
|
1657
1614
|
};
|
|
1658
|
-
vi.mocked(
|
|
1615
|
+
vi.mocked(ideContextStore.get).mockReturnValue(contextC);
|
|
1659
1616
|
// Act: Send a new, regular user message
|
|
1660
1617
|
stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
|
|
1661
1618
|
for await (const _ of stream) {
|
|
@@ -1687,11 +1644,6 @@ ${JSON.stringify({
|
|
|
1687
1644
|
getHistory: vi.fn().mockReturnValue([]),
|
|
1688
1645
|
};
|
|
1689
1646
|
client['chat'] = mockChat;
|
|
1690
|
-
const mockGenerator = {
|
|
1691
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
1692
|
-
generateContent: mockGenerateContentFn,
|
|
1693
|
-
};
|
|
1694
|
-
client['contentGenerator'] = mockGenerator;
|
|
1695
1647
|
// Act
|
|
1696
1648
|
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
|
|
1697
1649
|
for await (const _ of stream) {
|
|
@@ -1717,11 +1669,6 @@ ${JSON.stringify({
|
|
|
1717
1669
|
getHistory: vi.fn().mockReturnValue([]),
|
|
1718
1670
|
};
|
|
1719
1671
|
client['chat'] = mockChat;
|
|
1720
|
-
const mockGenerator = {
|
|
1721
|
-
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
|
1722
|
-
generateContent: mockGenerateContentFn,
|
|
1723
|
-
};
|
|
1724
|
-
client['contentGenerator'] = mockGenerator;
|
|
1725
1672
|
// Act
|
|
1726
1673
|
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
|
|
1727
1674
|
for await (const _ of stream) {
|
|
@@ -1730,21 +1677,44 @@ ${JSON.stringify({
|
|
|
1730
1677
|
// Assert
|
|
1731
1678
|
expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
|
|
1732
1679
|
});
|
|
1680
|
+
it('should abort linked signal when loop is detected', async () => {
|
|
1681
|
+
// Arrange
|
|
1682
|
+
vi.spyOn(client['loopDetector'], 'turnStarted').mockResolvedValue(false);
|
|
1683
|
+
vi.spyOn(client['loopDetector'], 'addAndCheck')
|
|
1684
|
+
.mockReturnValueOnce(false)
|
|
1685
|
+
.mockReturnValueOnce(true);
|
|
1686
|
+
let capturedSignal;
|
|
1687
|
+
mockTurnRunFn.mockImplementation((model, request, signal) => {
|
|
1688
|
+
capturedSignal = signal;
|
|
1689
|
+
return (async function* () {
|
|
1690
|
+
yield { type: 'content', value: 'First event' };
|
|
1691
|
+
yield { type: 'content', value: 'Second event' };
|
|
1692
|
+
})();
|
|
1693
|
+
});
|
|
1694
|
+
const mockChat = {
|
|
1695
|
+
addHistory: vi.fn(),
|
|
1696
|
+
getHistory: vi.fn().mockReturnValue([]),
|
|
1697
|
+
};
|
|
1698
|
+
client['chat'] = mockChat;
|
|
1699
|
+
// Act
|
|
1700
|
+
const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop');
|
|
1701
|
+
const events = [];
|
|
1702
|
+
for await (const event of stream) {
|
|
1703
|
+
events.push(event);
|
|
1704
|
+
}
|
|
1705
|
+
// Assert
|
|
1706
|
+
expect(events).toContainEqual({ type: GeminiEventType.LoopDetected });
|
|
1707
|
+
expect(capturedSignal.aborted).toBe(true);
|
|
1708
|
+
});
|
|
1733
1709
|
});
|
|
1734
1710
|
describe('generateContent', () => {
|
|
1735
1711
|
it('should call generateContent with the correct parameters', async () => {
|
|
1736
1712
|
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
|
1737
1713
|
const generationConfig = { temperature: 0.5 };
|
|
1738
1714
|
const abortSignal = new AbortController().signal;
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
generateContent: mockGenerateContentFn,
|
|
1743
|
-
};
|
|
1744
|
-
client['contentGenerator'] = mockGenerator;
|
|
1745
|
-
await client.generateContent(contents, generationConfig, abortSignal);
|
|
1746
|
-
expect(mockGenerateContentFn).toHaveBeenCalledWith({
|
|
1747
|
-
model: 'test-model',
|
|
1715
|
+
await client.generateContent(contents, generationConfig, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
|
|
1716
|
+
expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
|
|
1717
|
+
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
1748
1718
|
config: {
|
|
1749
1719
|
abortSignal,
|
|
1750
1720
|
systemInstruction: getCoreSystemPrompt(''),
|
|
@@ -1759,98 +1729,29 @@ ${JSON.stringify({
|
|
|
1759
1729
|
const contents = [{ role: 'user', parts: [{ text: 'test' }] }];
|
|
1760
1730
|
const currentModel = initialModel + '-changed';
|
|
1761
1731
|
vi.spyOn(client['config'], 'getModel').mockReturnValueOnce(currentModel);
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
generateContent: mockGenerateContentFn,
|
|
1765
|
-
};
|
|
1766
|
-
client['contentGenerator'] = mockGenerator;
|
|
1767
|
-
await client.generateContent(contents, {}, new AbortController().signal);
|
|
1768
|
-
expect(mockGenerateContentFn).not.toHaveBeenCalledWith({
|
|
1732
|
+
await client.generateContent(contents, {}, new AbortController().signal, DEFAULT_GEMINI_FLASH_MODEL);
|
|
1733
|
+
expect(mockContentGenerator.generateContent).not.toHaveBeenCalledWith({
|
|
1769
1734
|
model: initialModel,
|
|
1770
1735
|
config: expect.any(Object),
|
|
1771
1736
|
contents,
|
|
1772
1737
|
});
|
|
1773
|
-
expect(
|
|
1774
|
-
model:
|
|
1738
|
+
expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
|
|
1739
|
+
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
1775
1740
|
config: expect.any(Object),
|
|
1776
1741
|
contents,
|
|
1777
1742
|
}, 'test-session-id');
|
|
1778
1743
|
});
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const
|
|
1783
|
-
const
|
|
1784
|
-
//
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
client['config'].setModel = vi.fn();
|
|
1791
|
-
const result = await client['handleFlashFallback'](AuthType.LOGIN_WITH_GOOGLE);
|
|
1792
|
-
expect(result).toBe(fallbackModel);
|
|
1793
|
-
expect(mockFallbackHandler).toHaveBeenCalledWith(currentModel, fallbackModel, undefined);
|
|
1794
|
-
});
|
|
1795
|
-
});
|
|
1796
|
-
describe('setHistory', () => {
|
|
1797
|
-
it('should strip thought signatures when stripThoughts is true', () => {
|
|
1798
|
-
const mockChat = {
|
|
1799
|
-
setHistory: vi.fn(),
|
|
1800
|
-
};
|
|
1801
|
-
client['chat'] = mockChat;
|
|
1802
|
-
const historyWithThoughts = [
|
|
1803
|
-
{
|
|
1804
|
-
role: 'user',
|
|
1805
|
-
parts: [{ text: 'hello' }],
|
|
1806
|
-
},
|
|
1807
|
-
{
|
|
1808
|
-
role: 'model',
|
|
1809
|
-
parts: [
|
|
1810
|
-
{ text: 'thinking...', thoughtSignature: 'thought-123' },
|
|
1811
|
-
{
|
|
1812
|
-
functionCall: { name: 'test', args: {} },
|
|
1813
|
-
thoughtSignature: 'thought-456',
|
|
1814
|
-
},
|
|
1815
|
-
],
|
|
1816
|
-
},
|
|
1817
|
-
];
|
|
1818
|
-
client.setHistory(historyWithThoughts, { stripThoughts: true });
|
|
1819
|
-
const expectedHistory = [
|
|
1820
|
-
{
|
|
1821
|
-
role: 'user',
|
|
1822
|
-
parts: [{ text: 'hello' }],
|
|
1823
|
-
},
|
|
1824
|
-
{
|
|
1825
|
-
role: 'model',
|
|
1826
|
-
parts: [
|
|
1827
|
-
{ text: 'thinking...' },
|
|
1828
|
-
{ functionCall: { name: 'test', args: {} } },
|
|
1829
|
-
],
|
|
1830
|
-
},
|
|
1831
|
-
];
|
|
1832
|
-
expect(mockChat.setHistory).toHaveBeenCalledWith(expectedHistory);
|
|
1833
|
-
});
|
|
1834
|
-
it('should not strip thought signatures when stripThoughts is false', () => {
|
|
1835
|
-
const mockChat = {
|
|
1836
|
-
setHistory: vi.fn(),
|
|
1837
|
-
};
|
|
1838
|
-
client['chat'] = mockChat;
|
|
1839
|
-
const historyWithThoughts = [
|
|
1840
|
-
{
|
|
1841
|
-
role: 'user',
|
|
1842
|
-
parts: [{ text: 'hello' }],
|
|
1843
|
-
},
|
|
1844
|
-
{
|
|
1845
|
-
role: 'model',
|
|
1846
|
-
parts: [
|
|
1847
|
-
{ text: 'thinking...', thoughtSignature: 'thought-123' },
|
|
1848
|
-
{ text: 'ok', thoughtSignature: 'thought-456' },
|
|
1849
|
-
],
|
|
1850
|
-
},
|
|
1851
|
-
];
|
|
1852
|
-
client.setHistory(historyWithThoughts, { stripThoughts: false });
|
|
1853
|
-
expect(mockChat.setHistory).toHaveBeenCalledWith(historyWithThoughts);
|
|
1744
|
+
it('should use the Flash model when fallback mode is active', async () => {
|
|
1745
|
+
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
|
1746
|
+
const generationConfig = { temperature: 0.5 };
|
|
1747
|
+
const abortSignal = new AbortController().signal;
|
|
1748
|
+
const requestedModel = 'gemini-2.5-pro'; // A non-flash model
|
|
1749
|
+
// Mock config to be in fallback mode
|
|
1750
|
+
vi.spyOn(client['config'], 'isInFallbackMode').mockReturnValue(true);
|
|
1751
|
+
await client.generateContent(contents, generationConfig, abortSignal, requestedModel);
|
|
1752
|
+
expect(mockGenerateContentFn).toHaveBeenCalledWith(expect.objectContaining({
|
|
1753
|
+
model: DEFAULT_GEMINI_FLASH_MODEL,
|
|
1754
|
+
}), 'test-session-id');
|
|
1854
1755
|
});
|
|
1855
1756
|
});
|
|
1856
1757
|
});
|