@google/gemini-cli-core 0.5.0-nightly.20250906.968e9389 → 0.5.0-nightly.20250909.2b05cf3b

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.
Files changed (89) hide show
  1. package/dist/google-gemini-cli-core-0.3.4.tgz +0 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/code_assist/codeAssist.js +1 -1
  6. package/dist/src/code_assist/codeAssist.js.map +1 -1
  7. package/dist/src/code_assist/server.d.ts +1 -1
  8. package/dist/src/code_assist/server.js +24 -1
  9. package/dist/src/code_assist/server.js.map +1 -1
  10. package/dist/src/code_assist/server.test.js +25 -0
  11. package/dist/src/code_assist/server.test.js.map +1 -1
  12. package/dist/src/code_assist/types.d.ts +17 -2
  13. package/dist/src/config/config.d.ts +8 -4
  14. package/dist/src/config/config.js +42 -27
  15. package/dist/src/config/config.js.map +1 -1
  16. package/dist/src/config/config.test.js +81 -121
  17. package/dist/src/config/config.test.js.map +1 -1
  18. package/dist/src/config/storage.d.ts +1 -0
  19. package/dist/src/config/storage.js +4 -0
  20. package/dist/src/config/storage.js.map +1 -1
  21. package/dist/src/config/storage.test.js +4 -0
  22. package/dist/src/config/storage.test.js.map +1 -1
  23. package/dist/src/core/client.d.ts +4 -15
  24. package/dist/src/core/client.js +56 -100
  25. package/dist/src/core/client.js.map +1 -1
  26. package/dist/src/core/client.test.js +101 -321
  27. package/dist/src/core/client.test.js.map +1 -1
  28. package/dist/src/core/geminiChat.d.ts +2 -8
  29. package/dist/src/core/geminiChat.js +50 -52
  30. package/dist/src/core/geminiChat.js.map +1 -1
  31. package/dist/src/core/geminiChat.test.js +247 -47
  32. package/dist/src/core/geminiChat.test.js.map +1 -1
  33. package/dist/src/core/subagent.js +1 -3
  34. package/dist/src/core/subagent.js.map +1 -1
  35. package/dist/src/core/subagent.test.js +3 -5
  36. package/dist/src/core/subagent.test.js.map +1 -1
  37. package/dist/src/fallback/handler.d.ts +7 -0
  38. package/dist/src/fallback/handler.js +51 -0
  39. package/dist/src/fallback/handler.js.map +1 -0
  40. package/dist/src/fallback/handler.test.js +130 -0
  41. package/dist/src/fallback/handler.test.js.map +1 -0
  42. package/dist/src/fallback/types.d.ts +14 -0
  43. package/dist/src/fallback/types.js +7 -0
  44. package/dist/src/fallback/types.js.map +1 -0
  45. package/dist/src/generated/git-commit.d.ts +2 -2
  46. package/dist/src/generated/git-commit.js +2 -2
  47. package/dist/src/ide/process-utils.js +8 -1
  48. package/dist/src/ide/process-utils.js.map +1 -1
  49. package/dist/src/index.d.ts +1 -0
  50. package/dist/src/index.js +1 -0
  51. package/dist/src/index.js.map +1 -1
  52. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +2 -0
  53. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +7 -0
  54. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  55. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +15 -0
  56. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  57. package/dist/src/telemetry/constants.d.ts +1 -0
  58. package/dist/src/telemetry/constants.js +1 -0
  59. package/dist/src/telemetry/constants.js.map +1 -1
  60. package/dist/src/telemetry/loggers.d.ts +2 -1
  61. package/dist/src/telemetry/loggers.js +18 -1
  62. package/dist/src/telemetry/loggers.js.map +1 -1
  63. package/dist/src/telemetry/loggers.test.js +64 -5
  64. package/dist/src/telemetry/loggers.test.js.map +1 -1
  65. package/dist/src/telemetry/types.d.ts +6 -0
  66. package/dist/src/telemetry/types.js +14 -0
  67. package/dist/src/telemetry/types.js.map +1 -1
  68. package/dist/src/tools/mcp-client-manager.js +5 -21
  69. package/dist/src/tools/mcp-client-manager.js.map +1 -1
  70. package/dist/src/tools/ripGrep.d.ts +4 -0
  71. package/dist/src/tools/ripGrep.js +17 -2
  72. package/dist/src/tools/ripGrep.js.map +1 -1
  73. package/dist/src/tools/ripGrep.test.js +57 -5
  74. package/dist/src/tools/ripGrep.test.js.map +1 -1
  75. package/dist/src/utils/fileUtils.d.ts +1 -0
  76. package/dist/src/utils/fileUtils.js +10 -0
  77. package/dist/src/utils/fileUtils.js.map +1 -1
  78. package/dist/src/utils/fileUtils.test.js +17 -1
  79. package/dist/src/utils/fileUtils.test.js.map +1 -1
  80. package/dist/src/utils/flashFallback.test.d.ts +6 -0
  81. package/dist/src/utils/{flashFallback.integration.test.js → flashFallback.test.js} +31 -27
  82. package/dist/src/utils/flashFallback.test.js.map +1 -0
  83. package/dist/src/utils/nextSpeakerChecker.test.js +13 -42
  84. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
  85. package/dist/tsconfig.tsbuildinfo +1 -1
  86. package/package.json +3 -2
  87. package/dist/google-gemini-cli-core-0.3.1.tgz +0 -0
  88. package/dist/src/utils/flashFallback.integration.test.js.map +0 -1
  89. /package/dist/src/{utils/flashFallback.integration.test.d.ts → fallback/handler.test.d.ts} +0 -0
@@ -4,11 +4,9 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
7
- import { GoogleGenAI } from '@google/genai';
8
7
  import { findIndexAfterFraction, 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';
@@ -41,11 +39,7 @@ vi.mock('node:fs', () => {
41
39
  };
42
40
  });
43
41
  // --- Mocks ---
44
- const mockChatCreateFn = vi.fn();
45
- const mockGenerateContentFn = vi.fn();
46
- const mockEmbedContentFn = vi.fn();
47
42
  const mockTurnRunFn = vi.fn();
48
- vi.mock('@google/genai');
49
43
  vi.mock('./turn', async (importOriginal) => {
50
44
  const actual = await importOriginal();
51
45
  // Define a mock class that has the same shape as the real Turn
@@ -168,33 +162,24 @@ describe('isThinkingDefault', () => {
168
162
  });
169
163
  });
170
164
  describe('Gemini Client (client.ts)', () => {
165
+ let mockContentGenerator;
166
+ let mockConfig;
171
167
  let client;
168
+ let mockGenerateContentFn;
172
169
  beforeEach(async () => {
173
170
  vi.resetAllMocks();
171
+ mockGenerateContentFn = vi.fn().mockResolvedValue({
172
+ candidates: [{ content: { parts: [{ text: '{"key": "value"}' }] } }],
173
+ });
174
174
  // Disable 429 simulation for tests
175
175
  setSimulate429(false);
176
- // Set up the mock for GoogleGenAI constructor and its methods
177
- const MockedGoogleGenAI = vi.mocked(GoogleGenAI);
178
- MockedGoogleGenAI.mockImplementation(() => {
179
- const mock = {
180
- chats: { create: mockChatCreateFn },
181
- models: {
182
- generateContent: mockGenerateContentFn,
183
- embedContent: mockEmbedContentFn,
184
- },
185
- };
186
- return mock;
187
- });
188
- mockChatCreateFn.mockResolvedValue({});
189
- mockGenerateContentFn.mockResolvedValue({
190
- candidates: [
191
- {
192
- content: {
193
- parts: [{ text: '{"key": "value"}' }],
194
- },
195
- },
196
- ],
197
- });
176
+ mockContentGenerator = {
177
+ generateContent: mockGenerateContentFn,
178
+ generateContentStream: vi.fn(),
179
+ countTokens: vi.fn(),
180
+ embedContent: vi.fn(),
181
+ batchEmbedContents: vi.fn(),
182
+ };
198
183
  // Because the GeminiClient constructor kicks off an async process (startChat)
199
184
  // that depends on a fully-formed Config object, we need to mock the
200
185
  // entire implementation of Config for these tests.
@@ -209,7 +194,7 @@ describe('Gemini Client (client.ts)', () => {
209
194
  vertexai: false,
210
195
  authType: AuthType.USE_GEMINI,
211
196
  };
212
- const mockConfigObject = {
197
+ mockConfig = {
213
198
  getContentGeneratorConfig: vi
214
199
  .fn()
215
200
  .mockReturnValue(contentGeneratorConfig),
@@ -237,6 +222,7 @@ describe('Gemini Client (client.ts)', () => {
237
222
  getDirectories: vi.fn().mockReturnValue(['/test/dir']),
238
223
  }),
239
224
  getGeminiClient: vi.fn(),
225
+ isInFallbackMode: vi.fn().mockReturnValue(false),
240
226
  setFallbackMode: vi.fn(),
241
227
  getChatCompression: vi.fn().mockReturnValue(undefined),
242
228
  getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
@@ -245,36 +231,15 @@ describe('Gemini Client (client.ts)', () => {
245
231
  storage: {
246
232
  getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
247
233
  },
234
+ getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
248
235
  };
249
- const MockedConfig = vi.mocked(Config, true);
250
- MockedConfig.mockImplementation(() => mockConfigObject);
251
- // We can instantiate the client here since Config is mocked
252
- // and the constructor will use the mocked GoogleGenAI
253
- client = new GeminiClient(new Config({ sessionId: 'test-session-id' }));
254
- mockConfigObject.getGeminiClient.mockReturnValue(client);
255
- await client.initialize(contentGeneratorConfig);
236
+ client = new GeminiClient(mockConfig);
237
+ await client.initialize();
238
+ vi.mocked(mockConfig.getGeminiClient).mockReturnValue(client);
256
239
  });
257
240
  afterEach(() => {
258
241
  vi.restoreAllMocks();
259
242
  });
260
- // NOTE: The following tests for startChat were removed due to persistent issues with
261
- // the @google/genai mock. Specifically, the mockChatCreateFn (representing instance.chats.create)
262
- // was not being detected as called by the GeminiClient instance.
263
- // This likely points to a subtle issue in how the GoogleGenerativeAI class constructor
264
- // and its instance methods are mocked and then used by the class under test.
265
- // For future debugging, ensure that the `this.client` in `GeminiClient` (which is an
266
- // instance of the mocked GoogleGenerativeAI) correctly has its `chats.create` method
267
- // pointing to `mockChatCreateFn`.
268
- // it('startChat should call getCoreSystemPrompt with userMemory and pass to chats.create', async () => { ... });
269
- // it('startChat should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
270
- // NOTE: The following tests for generateJson were removed due to persistent issues with
271
- // the @google/genai mock, similar to the startChat tests. The mockGenerateContentFn
272
- // (representing instance.models.generateContent) was not being detected as called, or the mock
273
- // was not preventing an actual API call (leading to API key errors).
274
- // For future debugging, ensure `this.client.models.generateContent` in `GeminiClient` correctly
275
- // uses the `mockGenerateContentFn`.
276
- // it('generateJson should call getCoreSystemPrompt with userMemory and pass to generateContent', async () => { ... });
277
- // it('generateJson should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
278
243
  describe('generateEmbedding', () => {
279
244
  const texts = ['hello world', 'goodbye world'];
280
245
  const testEmbeddingModel = 'test-embedding-model';
@@ -283,16 +248,15 @@ describe('Gemini Client (client.ts)', () => {
283
248
  [0.1, 0.2, 0.3],
284
249
  [0.4, 0.5, 0.6],
285
250
  ];
286
- const mockResponse = {
251
+ vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
287
252
  embeddings: [
288
253
  { values: mockEmbeddings[0] },
289
254
  { values: mockEmbeddings[1] },
290
255
  ],
291
- };
292
- mockEmbedContentFn.mockResolvedValue(mockResponse);
256
+ });
293
257
  const result = await client.generateEmbedding(texts);
294
- expect(mockEmbedContentFn).toHaveBeenCalledTimes(1);
295
- expect(mockEmbedContentFn).toHaveBeenCalledWith({
258
+ expect(mockContentGenerator.embedContent).toHaveBeenCalledTimes(1);
259
+ expect(mockContentGenerator.embedContent).toHaveBeenCalledWith({
296
260
  model: testEmbeddingModel,
297
261
  contents: texts,
298
262
  });
@@ -301,43 +265,38 @@ describe('Gemini Client (client.ts)', () => {
301
265
  it('should return an empty array if an empty array is passed', async () => {
302
266
  const result = await client.generateEmbedding([]);
303
267
  expect(result).toEqual([]);
304
- expect(mockEmbedContentFn).not.toHaveBeenCalled();
268
+ expect(mockContentGenerator.embedContent).not.toHaveBeenCalled();
305
269
  });
306
270
  it('should throw an error if API response has no embeddings array', async () => {
307
- mockEmbedContentFn.mockResolvedValue({}); // No `embeddings` key
271
+ vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({});
308
272
  await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
309
273
  });
310
274
  it('should throw an error if API response has an empty embeddings array', async () => {
311
- const mockResponse = {
275
+ vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
312
276
  embeddings: [],
313
- };
314
- mockEmbedContentFn.mockResolvedValue(mockResponse);
277
+ });
315
278
  await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
316
279
  });
317
280
  it('should throw an error if API returns a mismatched number of embeddings', async () => {
318
- const mockResponse = {
281
+ vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
319
282
  embeddings: [{ values: [1, 2, 3] }], // Only one for two texts
320
- };
321
- mockEmbedContentFn.mockResolvedValue(mockResponse);
283
+ });
322
284
  await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned a mismatched number of embeddings. Expected 2, got 1.');
323
285
  });
324
286
  it('should throw an error if any embedding has nullish values', async () => {
325
- const mockResponse = {
287
+ vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
326
288
  embeddings: [{ values: [1, 2, 3] }, { values: undefined }], // Second one is bad
327
- };
328
- mockEmbedContentFn.mockResolvedValue(mockResponse);
289
+ });
329
290
  await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 1: "goodbye world"');
330
291
  });
331
292
  it('should throw an error if any embedding has an empty values array', async () => {
332
- const mockResponse = {
293
+ vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
333
294
  embeddings: [{ values: [] }, { values: [1, 2, 3] }], // First one is bad
334
- };
335
- mockEmbedContentFn.mockResolvedValue(mockResponse);
295
+ });
336
296
  await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 0: "hello world"');
337
297
  });
338
298
  it('should propagate errors from the API call', async () => {
339
- const apiError = new Error('API Failure');
340
- mockEmbedContentFn.mockRejectedValue(apiError);
299
+ vi.mocked(mockContentGenerator.embedContent).mockRejectedValue(new Error('API Failure'));
341
300
  await expect(client.generateEmbedding(texts)).rejects.toThrow('API Failure');
342
301
  });
343
302
  });
@@ -346,14 +305,11 @@ describe('Gemini Client (client.ts)', () => {
346
305
  const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
347
306
  const schema = { type: 'string' };
348
307
  const abortSignal = new AbortController().signal;
349
- // Mock countTokens
350
- const mockGenerator = {
351
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
352
- generateContent: mockGenerateContentFn,
353
- };
354
- client['contentGenerator'] = mockGenerator;
308
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
309
+ totalTokens: 1,
310
+ });
355
311
  await client.generateJson(contents, schema, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
356
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
312
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
357
313
  model: DEFAULT_GEMINI_FLASH_MODEL,
358
314
  config: {
359
315
  abortSignal,
@@ -374,13 +330,11 @@ describe('Gemini Client (client.ts)', () => {
374
330
  const abortSignal = new AbortController().signal;
375
331
  const customModel = 'custom-json-model';
376
332
  const customConfig = { temperature: 0.9, topK: 20 };
377
- const mockGenerator = {
378
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
379
- generateContent: mockGenerateContentFn,
380
- };
381
- client['contentGenerator'] = mockGenerator;
333
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
334
+ totalTokens: 1,
335
+ });
382
336
  await client.generateJson(contents, schema, abortSignal, customModel, customConfig);
383
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
337
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
384
338
  model: customModel,
385
339
  config: {
386
340
  abortSignal,
@@ -394,6 +348,20 @@ describe('Gemini Client (client.ts)', () => {
394
348
  contents,
395
349
  }, 'test-session-id');
396
350
  });
351
+ it('should use the Flash model when fallback mode is active', async () => {
352
+ const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
353
+ const schema = { type: 'string' };
354
+ const abortSignal = new AbortController().signal;
355
+ const requestedModel = 'gemini-2.5-pro'; // A non-flash model
356
+ // Mock config to be in fallback mode
357
+ // We access the mock via the client instance which holds the mocked config
358
+ vi.spyOn(client['config'], 'isInFallbackMode').mockReturnValue(true);
359
+ await client.generateJson(contents, schema, abortSignal, requestedModel);
360
+ // Assert that the Flash model was used, not the requested model
361
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith(expect.objectContaining({
362
+ model: DEFAULT_GEMINI_FLASH_MODEL,
363
+ }), 'test-session-id');
364
+ });
397
365
  });
398
366
  describe('addHistory', () => {
399
367
  it('should call chat.addHistory with the provided content', async () => {
@@ -432,16 +400,12 @@ describe('Gemini Client (client.ts)', () => {
432
400
  });
433
401
  });
434
402
  describe('tryCompressChat', () => {
435
- const mockCountTokens = vi.fn();
436
403
  const mockSendMessage = vi.fn();
437
404
  const mockGetHistory = vi.fn();
438
405
  beforeEach(() => {
439
406
  vi.mock('./tokenLimits', () => ({
440
407
  tokenLimit: vi.fn(),
441
408
  }));
442
- client['contentGenerator'] = {
443
- countTokens: mockCountTokens,
444
- };
445
409
  client['chat'] = {
446
410
  getHistory: mockGetHistory,
447
411
  addHistory: vi.fn(),
@@ -458,23 +422,17 @@ describe('Gemini Client (client.ts)', () => {
458
422
  setHistory: vi.fn(),
459
423
  sendMessage: vi.fn().mockResolvedValue({ text: 'Summary' }),
460
424
  };
461
- const mockCountTokens = vi
462
- .fn()
425
+ vi.mocked(mockContentGenerator.countTokens)
463
426
  .mockResolvedValueOnce({ totalTokens: 1000 })
464
427
  .mockResolvedValueOnce({ totalTokens: 5000 });
465
- const mockGenerator = {
466
- countTokens: mockCountTokens,
467
- };
468
428
  client['chat'] = mockChat;
469
- client['contentGenerator'] = mockGenerator;
470
429
  client['startChat'] = vi.fn().mockResolvedValue({ ...mockChat });
471
- return { client, mockChat, mockGenerator };
430
+ return { client, mockChat };
472
431
  }
473
432
  describe('when compression inflates the token count', () => {
474
- it('uses the truncated history for compression');
475
433
  it('allows compression to be forced/manual after a failure', async () => {
476
- const { client, mockGenerator } = setup();
477
- mockGenerator.countTokens?.mockResolvedValue({
434
+ const { client } = setup();
435
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
478
436
  totalTokens: 1000,
479
437
  });
480
438
  await client.tryCompressChat('prompt-id-4'); // Fails
@@ -487,6 +445,9 @@ describe('Gemini Client (client.ts)', () => {
487
445
  });
488
446
  it('yields the result even if the compression inflated the tokens', async () => {
489
447
  const { client } = setup();
448
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
449
+ totalTokens: 1000,
450
+ });
490
451
  const result = await client.tryCompressChat('prompt-id-4', true);
491
452
  expect(result).toEqual({
492
453
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
@@ -501,7 +462,7 @@ describe('Gemini Client (client.ts)', () => {
501
462
  });
502
463
  it('restores the history back to the original', async () => {
503
464
  vi.mocked(tokenLimit).mockReturnValue(1000);
504
- mockCountTokens.mockResolvedValue({
465
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
505
466
  totalTokens: 999,
506
467
  });
507
468
  const originalHistory = [
@@ -517,11 +478,11 @@ describe('Gemini Client (client.ts)', () => {
517
478
  expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
518
479
  });
519
480
  it('will not attempt to compress context after a failure', async () => {
520
- const { client, mockGenerator } = setup();
481
+ const { client } = setup();
521
482
  await client.tryCompressChat('prompt-id-4');
522
483
  const result = await client.tryCompressChat('prompt-id-5');
523
484
  // it counts tokens for {original, compressed} and then never again
524
- expect(mockGenerator.countTokens).toHaveBeenCalledTimes(2);
485
+ expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
525
486
  expect(result).toEqual({
526
487
  compressionStatus: CompressionStatus.NOOP,
527
488
  newTokenCount: 0,
@@ -529,33 +490,13 @@ describe('Gemini Client (client.ts)', () => {
529
490
  });
530
491
  });
531
492
  });
532
- it('attempts to compress with a maxOutputTokens set to the original token count', async () => {
533
- vi.mocked(tokenLimit).mockReturnValue(1000);
534
- mockCountTokens.mockResolvedValue({
535
- totalTokens: 999,
536
- });
537
- mockGetHistory.mockReturnValue([
538
- { role: 'user', parts: [{ text: '...history...' }] },
539
- ]);
540
- // Mock the summary response from the chat
541
- mockSendMessage.mockResolvedValue({
542
- role: 'model',
543
- parts: [{ text: 'This is a summary.' }],
544
- });
545
- await client.tryCompressChat('prompt-id-2', true);
546
- expect(mockSendMessage).toHaveBeenCalledWith(expect.objectContaining({
547
- config: expect.objectContaining({
548
- maxOutputTokens: 999,
549
- }),
550
- }), 'prompt-id-2');
551
- });
552
493
  it('should not trigger summarization if token count is below threshold', async () => {
553
494
  const MOCKED_TOKEN_LIMIT = 1000;
554
495
  vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
555
496
  mockGetHistory.mockReturnValue([
556
497
  { role: 'user', parts: [{ text: '...history...' }] },
557
498
  ]);
558
- mockCountTokens.mockResolvedValue({
499
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
559
500
  totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
560
501
  });
561
502
  const initialChat = client.getChat();
@@ -582,7 +523,7 @@ describe('Gemini Client (client.ts)', () => {
582
523
  ]);
583
524
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
584
525
  const newTokenCount = 100;
585
- mockCountTokens
526
+ vi.mocked(mockContentGenerator.countTokens)
586
527
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
587
528
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
588
529
  // Mock the summary response from the chat
@@ -608,7 +549,7 @@ describe('Gemini Client (client.ts)', () => {
608
549
  ]);
609
550
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
610
551
  const newTokenCount = 100;
611
- mockCountTokens
552
+ vi.mocked(mockContentGenerator.countTokens)
612
553
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
613
554
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
614
555
  // Mock the summary response from the chat
@@ -653,7 +594,7 @@ describe('Gemini Client (client.ts)', () => {
653
594
  ]);
654
595
  const originalTokenCount = 1000 * 0.7;
655
596
  const newTokenCount = 100;
656
- mockCountTokens
597
+ vi.mocked(mockContentGenerator.countTokens)
657
598
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
658
599
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
659
600
  // Mock the summary response from the chat
@@ -687,7 +628,7 @@ describe('Gemini Client (client.ts)', () => {
687
628
  ]);
688
629
  const originalTokenCount = 10; // Well below threshold
689
630
  const newTokenCount = 5;
690
- mockCountTokens
631
+ vi.mocked(mockContentGenerator.countTokens)
691
632
  .mockResolvedValueOnce({ totalTokens: originalTokenCount })
692
633
  .mockResolvedValueOnce({ totalTokens: newTokenCount });
693
634
  // Mock the summary response from the chat
@@ -708,9 +649,14 @@ describe('Gemini Client (client.ts)', () => {
708
649
  expect(newChat).not.toBe(initialChat);
709
650
  });
710
651
  it('should use current model from config for token counting after sendMessage', async () => {
711
- const initialModel = client['config'].getModel();
712
- const mockCountTokens = vi
713
- .fn()
652
+ const initialModel = mockConfig.getModel();
653
+ // mock the model has been changed between calls of `countTokens`
654
+ const firstCurrentModel = initialModel + '-changed-1';
655
+ const secondCurrentModel = initialModel + '-changed-2';
656
+ vi.mocked(mockConfig.getModel)
657
+ .mockReturnValueOnce(firstCurrentModel)
658
+ .mockReturnValueOnce(secondCurrentModel);
659
+ vi.mocked(mockContentGenerator.countTokens)
714
660
  .mockResolvedValueOnce({ totalTokens: 100000 })
715
661
  .mockResolvedValueOnce({ totalTokens: 5000 });
716
662
  const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
@@ -723,25 +669,15 @@ describe('Gemini Client (client.ts)', () => {
723
669
  setHistory: vi.fn(),
724
670
  sendMessage: mockSendMessage,
725
671
  };
726
- const mockGenerator = {
727
- countTokens: mockCountTokens,
728
- };
729
- // mock the model has been changed between calls of `countTokens`
730
- const firstCurrentModel = initialModel + '-changed-1';
731
- const secondCurrentModel = initialModel + '-changed-2';
732
- vi.spyOn(client['config'], 'getModel')
733
- .mockReturnValueOnce(firstCurrentModel)
734
- .mockReturnValueOnce(secondCurrentModel);
735
672
  client['chat'] = mockChat;
736
- client['contentGenerator'] = mockGenerator;
737
673
  client['startChat'] = vi.fn().mockResolvedValue(mockChat);
738
674
  const result = await client.tryCompressChat('prompt-id-4', true);
739
- expect(mockCountTokens).toHaveBeenCalledTimes(2);
740
- expect(mockCountTokens).toHaveBeenNthCalledWith(1, {
675
+ expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
676
+ expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(1, {
741
677
  model: firstCurrentModel,
742
678
  contents: mockChatHistory,
743
679
  });
744
- expect(mockCountTokens).toHaveBeenNthCalledWith(2, {
680
+ expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(2, {
745
681
  model: secondCurrentModel,
746
682
  contents: expect.any(Array),
747
683
  });
@@ -755,20 +691,9 @@ describe('Gemini Client (client.ts)', () => {
755
691
  describe('sendMessageStream', () => {
756
692
  it('emits a compression event when the context was automatically compressed', async () => {
757
693
  // Arrange
758
- const mockStream = (async function* () {
694
+ mockTurnRunFn.mockReturnValue((async function* () {
759
695
  yield { type: 'content', value: 'Hello' };
760
- })();
761
- mockTurnRunFn.mockReturnValue(mockStream);
762
- const mockChat = {
763
- addHistory: vi.fn(),
764
- getHistory: vi.fn().mockReturnValue([]),
765
- };
766
- client['chat'] = mockChat;
767
- const mockGenerator = {
768
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
769
- generateContent: mockGenerateContentFn,
770
- };
771
- client['contentGenerator'] = mockGenerator;
696
+ })());
772
697
  const compressionInfo = {
773
698
  compressionStatus: CompressionStatus.COMPRESSED,
774
699
  originalTokenCount: 1000,
@@ -798,16 +723,6 @@ describe('Gemini Client (client.ts)', () => {
798
723
  yield { type: 'content', value: 'Hello' };
799
724
  })();
800
725
  mockTurnRunFn.mockReturnValue(mockStream);
801
- const mockChat = {
802
- addHistory: vi.fn(),
803
- getHistory: vi.fn().mockReturnValue([]),
804
- };
805
- client['chat'] = mockChat;
806
- const mockGenerator = {
807
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
808
- generateContent: mockGenerateContentFn,
809
- };
810
- client['contentGenerator'] = mockGenerator;
811
726
  const compressionInfo = {
812
727
  compressionStatus,
813
728
  originalTokenCount: 1000,
@@ -846,21 +761,15 @@ describe('Gemini Client (client.ts)', () => {
846
761
  ],
847
762
  },
848
763
  });
849
- vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
850
- const mockStream = (async function* () {
764
+ vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
765
+ mockTurnRunFn.mockReturnValue((async function* () {
851
766
  yield { type: 'content', value: 'Hello' };
852
- })();
853
- mockTurnRunFn.mockReturnValue(mockStream);
767
+ })());
854
768
  const mockChat = {
855
769
  addHistory: vi.fn(),
856
770
  getHistory: vi.fn().mockReturnValue([]),
857
771
  };
858
772
  client['chat'] = mockChat;
859
- const mockGenerator = {
860
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
861
- generateContent: mockGenerateContentFn,
862
- };
863
- client['contentGenerator'] = mockGenerator;
864
773
  const initialRequest = [{ text: 'Hi' }];
865
774
  // Act
866
775
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -908,11 +817,6 @@ ${JSON.stringify({
908
817
  getHistory: vi.fn().mockReturnValue([]),
909
818
  };
910
819
  client['chat'] = mockChat;
911
- const mockGenerator = {
912
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
913
- generateContent: mockGenerateContentFn,
914
- };
915
- client['contentGenerator'] = mockGenerator;
916
820
  const initialRequest = [{ text: 'Hi' }];
917
821
  // Act
918
822
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -948,11 +852,6 @@ ${JSON.stringify({
948
852
  getHistory: vi.fn().mockReturnValue([]),
949
853
  };
950
854
  client['chat'] = mockChat;
951
- const mockGenerator = {
952
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
953
- generateContent: mockGenerateContentFn,
954
- };
955
- client['contentGenerator'] = mockGenerator;
956
855
  const initialRequest = [{ text: 'Hi' }];
957
856
  // Act
958
857
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -1008,11 +907,6 @@ ${JSON.stringify({
1008
907
  getHistory: vi.fn().mockReturnValue([]),
1009
908
  };
1010
909
  client['chat'] = mockChat;
1011
- const mockGenerator = {
1012
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1013
- generateContent: mockGenerateContentFn,
1014
- };
1015
- client['contentGenerator'] = mockGenerator;
1016
910
  const initialRequest = [{ text: 'Hi' }];
1017
911
  // Act
1018
912
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -1046,11 +940,6 @@ ${JSON.stringify({
1046
940
  getHistory: vi.fn().mockReturnValue([]),
1047
941
  };
1048
942
  client['chat'] = mockChat;
1049
- const mockGenerator = {
1050
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1051
- generateContent: mockGenerateContentFn,
1052
- };
1053
- client['contentGenerator'] = mockGenerator;
1054
943
  // Act
1055
944
  const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
1056
945
  // Consume the stream manually to get the final return value.
@@ -1083,11 +972,6 @@ ${JSON.stringify({
1083
972
  getHistory: vi.fn().mockReturnValue([]),
1084
973
  };
1085
974
  client['chat'] = mockChat;
1086
- const mockGenerator = {
1087
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1088
- generateContent: mockGenerateContentFn,
1089
- };
1090
- client['contentGenerator'] = mockGenerator;
1091
975
  // Use a signal that never gets aborted
1092
976
  const abortController = new AbortController();
1093
977
  const signal = abortController.signal;
@@ -1150,11 +1034,6 @@ ${JSON.stringify({
1150
1034
  getHistory: vi.fn().mockReturnValue([]),
1151
1035
  };
1152
1036
  client['chat'] = mockChat;
1153
- const mockGenerator = {
1154
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1155
- generateContent: mockGenerateContentFn,
1156
- };
1157
- client['contentGenerator'] = mockGenerator;
1158
1037
  // Act & Assert
1159
1038
  // Run up to the limit
1160
1039
  for (let i = 0; i < MAX_SESSION_TURNS; i++) {
@@ -1193,11 +1072,6 @@ ${JSON.stringify({
1193
1072
  getHistory: vi.fn().mockReturnValue([]),
1194
1073
  };
1195
1074
  client['chat'] = mockChat;
1196
- const mockGenerator = {
1197
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1198
- generateContent: mockGenerateContentFn,
1199
- };
1200
- client['contentGenerator'] = mockGenerator;
1201
1075
  // Use a signal that never gets aborted
1202
1076
  const abortController = new AbortController();
1203
1077
  const signal = abortController.signal;
@@ -1261,11 +1135,6 @@ ${JSON.stringify({
1261
1135
  ]),
1262
1136
  };
1263
1137
  client['chat'] = mockChat;
1264
- const mockGenerator = {
1265
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1266
- generateContent: mockGenerateContentFn,
1267
- };
1268
- client['contentGenerator'] = mockGenerator;
1269
1138
  });
1270
1139
  const testCases = [
1271
1140
  {
@@ -1475,10 +1344,6 @@ ${JSON.stringify({
1475
1344
  sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1476
1345
  };
1477
1346
  client['chat'] = mockChat;
1478
- const mockGenerator = {
1479
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1480
- };
1481
- client['contentGenerator'] = mockGenerator;
1482
1347
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1483
1348
  vi.mocked(ideContext.getIdeContext).mockReturnValue({
1484
1349
  workspaceState: {
@@ -1742,11 +1607,6 @@ ${JSON.stringify({
1742
1607
  getHistory: vi.fn().mockReturnValue([]),
1743
1608
  };
1744
1609
  client['chat'] = mockChat;
1745
- const mockGenerator = {
1746
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1747
- generateContent: mockGenerateContentFn,
1748
- };
1749
- client['contentGenerator'] = mockGenerator;
1750
1610
  // Act
1751
1611
  const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1752
1612
  for await (const _ of stream) {
@@ -1772,11 +1632,6 @@ ${JSON.stringify({
1772
1632
  getHistory: vi.fn().mockReturnValue([]),
1773
1633
  };
1774
1634
  client['chat'] = mockChat;
1775
- const mockGenerator = {
1776
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1777
- generateContent: mockGenerateContentFn,
1778
- };
1779
- client['contentGenerator'] = mockGenerator;
1780
1635
  // Act
1781
1636
  const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1782
1637
  for await (const _ of stream) {
@@ -1791,14 +1646,8 @@ ${JSON.stringify({
1791
1646
  const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
1792
1647
  const generationConfig = { temperature: 0.5 };
1793
1648
  const abortSignal = new AbortController().signal;
1794
- // Mock countTokens
1795
- const mockGenerator = {
1796
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
1797
- generateContent: mockGenerateContentFn,
1798
- };
1799
- client['contentGenerator'] = mockGenerator;
1800
1649
  await client.generateContent(contents, generationConfig, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
1801
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
1650
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
1802
1651
  model: DEFAULT_GEMINI_FLASH_MODEL,
1803
1652
  config: {
1804
1653
  abortSignal,
@@ -1814,98 +1663,29 @@ ${JSON.stringify({
1814
1663
  const contents = [{ role: 'user', parts: [{ text: 'test' }] }];
1815
1664
  const currentModel = initialModel + '-changed';
1816
1665
  vi.spyOn(client['config'], 'getModel').mockReturnValueOnce(currentModel);
1817
- const mockGenerator = {
1818
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
1819
- generateContent: mockGenerateContentFn,
1820
- };
1821
- client['contentGenerator'] = mockGenerator;
1822
1666
  await client.generateContent(contents, {}, new AbortController().signal, DEFAULT_GEMINI_FLASH_MODEL);
1823
- expect(mockGenerateContentFn).not.toHaveBeenCalledWith({
1667
+ expect(mockContentGenerator.generateContent).not.toHaveBeenCalledWith({
1824
1668
  model: initialModel,
1825
1669
  config: expect.any(Object),
1826
1670
  contents,
1827
1671
  });
1828
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
1672
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
1829
1673
  model: DEFAULT_GEMINI_FLASH_MODEL,
1830
1674
  config: expect.any(Object),
1831
1675
  contents,
1832
1676
  }, 'test-session-id');
1833
1677
  });
1834
- });
1835
- describe('handleFlashFallback', () => {
1836
- it('should use current model from config when checking for fallback', async () => {
1837
- const initialModel = client['config'].getModel();
1838
- const fallbackModel = DEFAULT_GEMINI_FLASH_MODEL;
1839
- // mock config been changed
1840
- const currentModel = initialModel + '-changed';
1841
- const getModelSpy = vi.spyOn(client['config'], 'getModel');
1842
- getModelSpy.mockReturnValue(currentModel);
1843
- const mockFallbackHandler = vi.fn().mockResolvedValue(true);
1844
- client['config'].flashFallbackHandler = mockFallbackHandler;
1845
- client['config'].setModel = vi.fn();
1846
- const result = await client['handleFlashFallback'](AuthType.LOGIN_WITH_GOOGLE);
1847
- expect(result).toBe(fallbackModel);
1848
- expect(mockFallbackHandler).toHaveBeenCalledWith(currentModel, fallbackModel, undefined);
1849
- });
1850
- });
1851
- describe('setHistory', () => {
1852
- it('should strip thought signatures when stripThoughts is true', () => {
1853
- const mockChat = {
1854
- setHistory: vi.fn(),
1855
- };
1856
- client['chat'] = mockChat;
1857
- const historyWithThoughts = [
1858
- {
1859
- role: 'user',
1860
- parts: [{ text: 'hello' }],
1861
- },
1862
- {
1863
- role: 'model',
1864
- parts: [
1865
- { text: 'thinking...', thoughtSignature: 'thought-123' },
1866
- {
1867
- functionCall: { name: 'test', args: {} },
1868
- thoughtSignature: 'thought-456',
1869
- },
1870
- ],
1871
- },
1872
- ];
1873
- client.setHistory(historyWithThoughts, { stripThoughts: true });
1874
- const expectedHistory = [
1875
- {
1876
- role: 'user',
1877
- parts: [{ text: 'hello' }],
1878
- },
1879
- {
1880
- role: 'model',
1881
- parts: [
1882
- { text: 'thinking...' },
1883
- { functionCall: { name: 'test', args: {} } },
1884
- ],
1885
- },
1886
- ];
1887
- expect(mockChat.setHistory).toHaveBeenCalledWith(expectedHistory);
1888
- });
1889
- it('should not strip thought signatures when stripThoughts is false', () => {
1890
- const mockChat = {
1891
- setHistory: vi.fn(),
1892
- };
1893
- client['chat'] = mockChat;
1894
- const historyWithThoughts = [
1895
- {
1896
- role: 'user',
1897
- parts: [{ text: 'hello' }],
1898
- },
1899
- {
1900
- role: 'model',
1901
- parts: [
1902
- { text: 'thinking...', thoughtSignature: 'thought-123' },
1903
- { text: 'ok', thoughtSignature: 'thought-456' },
1904
- ],
1905
- },
1906
- ];
1907
- client.setHistory(historyWithThoughts, { stripThoughts: false });
1908
- expect(mockChat.setHistory).toHaveBeenCalledWith(historyWithThoughts);
1678
+ it('should use the Flash model when fallback mode is active', async () => {
1679
+ const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
1680
+ const generationConfig = { temperature: 0.5 };
1681
+ const abortSignal = new AbortController().signal;
1682
+ const requestedModel = 'gemini-2.5-pro'; // A non-flash model
1683
+ // Mock config to be in fallback mode
1684
+ vi.spyOn(client['config'], 'isInFallbackMode').mockReturnValue(true);
1685
+ await client.generateContent(contents, generationConfig, abortSignal, requestedModel);
1686
+ expect(mockGenerateContentFn).toHaveBeenCalledWith(expect.objectContaining({
1687
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1688
+ }), 'test-session-id');
1909
1689
  });
1910
1690
  });
1911
1691
  });