@google/gemini-cli-core 0.5.0-preview.1 → 0.7.0-nightly.20250912.68035591

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 (231) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +12 -2
  3. package/dist/google-gemini-cli-core-0.6.0-nightly.tgz +0 -0
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/src/config/config.d.ts +24 -1
  8. package/dist/src/config/config.js +58 -15
  9. package/dist/src/config/config.js.map +1 -1
  10. package/dist/src/config/config.test.js +11 -15
  11. package/dist/src/config/config.test.js.map +1 -1
  12. package/dist/src/config/models.d.ts +14 -0
  13. package/dist/src/config/models.js +26 -0
  14. package/dist/src/config/models.js.map +1 -1
  15. package/dist/src/config/models.test.d.ts +6 -0
  16. package/dist/src/config/models.test.js +55 -0
  17. package/dist/src/config/models.test.js.map +1 -0
  18. package/dist/src/confirmation-bus/index.d.ts +7 -0
  19. package/dist/src/confirmation-bus/index.js +8 -0
  20. package/dist/src/confirmation-bus/index.js.map +1 -0
  21. package/dist/src/confirmation-bus/message-bus.d.ts +17 -0
  22. package/dist/src/confirmation-bus/message-bus.js +81 -0
  23. package/dist/src/confirmation-bus/message-bus.js.map +1 -0
  24. package/dist/src/confirmation-bus/message-bus.test.d.ts +6 -0
  25. package/dist/src/confirmation-bus/message-bus.test.js +164 -0
  26. package/dist/src/confirmation-bus/message-bus.test.js.map +1 -0
  27. package/dist/src/confirmation-bus/types.d.ts +38 -0
  28. package/dist/src/confirmation-bus/types.js +15 -0
  29. package/dist/src/confirmation-bus/types.js.map +1 -0
  30. package/dist/src/core/client.d.ts +4 -1
  31. package/dist/src/core/client.js +46 -18
  32. package/dist/src/core/client.js.map +1 -1
  33. package/dist/src/core/client.test.js +156 -46
  34. package/dist/src/core/client.test.js.map +1 -1
  35. package/dist/src/core/contentGenerator.d.ts +0 -1
  36. package/dist/src/core/contentGenerator.js +0 -4
  37. package/dist/src/core/contentGenerator.js.map +1 -1
  38. package/dist/src/core/contentGenerator.test.js +0 -3
  39. package/dist/src/core/contentGenerator.test.js.map +1 -1
  40. package/dist/src/core/coreToolScheduler.d.ts +4 -3
  41. package/dist/src/core/coreToolScheduler.js +42 -5
  42. package/dist/src/core/coreToolScheduler.js.map +1 -1
  43. package/dist/src/core/coreToolScheduler.test.js +34 -0
  44. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  45. package/dist/src/core/geminiChat.d.ts +1 -22
  46. package/dist/src/core/geminiChat.js +15 -135
  47. package/dist/src/core/geminiChat.js.map +1 -1
  48. package/dist/src/core/geminiChat.test.js +59 -312
  49. package/dist/src/core/geminiChat.test.js.map +1 -1
  50. package/dist/src/core/nonInteractiveToolExecutor.test.js +48 -0
  51. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  52. package/dist/src/core/subagent.js +1 -1
  53. package/dist/src/core/subagent.js.map +1 -1
  54. package/dist/src/core/subagent.test.js +9 -8
  55. package/dist/src/core/subagent.test.js.map +1 -1
  56. package/dist/src/core/turn.d.ts +2 -1
  57. package/dist/src/core/turn.js +2 -2
  58. package/dist/src/core/turn.js.map +1 -1
  59. package/dist/src/core/turn.test.js +18 -18
  60. package/dist/src/core/turn.test.js.map +1 -1
  61. package/dist/src/generated/git-commit.d.ts +2 -2
  62. package/dist/src/generated/git-commit.js +2 -2
  63. package/dist/src/generated/git-commit.js.map +1 -1
  64. package/dist/src/ide/ide-client.d.ts +27 -0
  65. package/dist/src/ide/ide-client.js +85 -5
  66. package/dist/src/ide/ide-client.js.map +1 -1
  67. package/dist/src/ide/ide-client.test.js +53 -0
  68. package/dist/src/ide/ide-client.test.js.map +1 -1
  69. package/dist/src/ide/ideContext.d.ts +34 -20
  70. package/dist/src/ide/ideContext.js +20 -33
  71. package/dist/src/ide/ideContext.js.map +1 -1
  72. package/dist/src/ide/ideContext.test.js +37 -39
  73. package/dist/src/ide/ideContext.test.js.map +1 -1
  74. package/dist/src/index.d.ts +3 -1
  75. package/dist/src/index.js +3 -1
  76. package/dist/src/index.js.map +1 -1
  77. package/dist/src/output/json-formatter.d.ts +11 -0
  78. package/dist/src/output/json-formatter.js +30 -0
  79. package/dist/src/output/json-formatter.js.map +1 -0
  80. package/dist/src/output/json-formatter.test.d.ts +6 -0
  81. package/dist/src/output/json-formatter.test.js +266 -0
  82. package/dist/src/output/json-formatter.test.js.map +1 -0
  83. package/dist/src/output/types.d.ts +20 -0
  84. package/dist/src/output/types.js +11 -0
  85. package/dist/src/output/types.js.map +1 -0
  86. package/dist/src/policy/index.d.ts +7 -0
  87. package/dist/src/policy/index.js +8 -0
  88. package/dist/src/policy/index.js.map +1 -0
  89. package/dist/src/policy/policy-engine.d.ts +30 -0
  90. package/dist/src/policy/policy-engine.js +83 -0
  91. package/dist/src/policy/policy-engine.js.map +1 -0
  92. package/dist/src/policy/policy-engine.test.d.ts +6 -0
  93. package/dist/src/policy/policy-engine.test.js +470 -0
  94. package/dist/src/policy/policy-engine.test.js.map +1 -0
  95. package/dist/src/policy/stable-stringify.d.ts +58 -0
  96. package/dist/src/policy/stable-stringify.js +122 -0
  97. package/dist/src/policy/stable-stringify.js.map +1 -0
  98. package/dist/src/policy/types.d.ts +47 -0
  99. package/dist/src/policy/types.js +12 -0
  100. package/dist/src/policy/types.js.map +1 -0
  101. package/dist/src/routing/modelRouterService.d.ts +23 -0
  102. package/dist/src/routing/modelRouterService.js +36 -0
  103. package/dist/src/routing/modelRouterService.js.map +1 -0
  104. package/dist/src/routing/modelRouterService.test.d.ts +6 -0
  105. package/dist/src/routing/modelRouterService.test.js +72 -0
  106. package/dist/src/routing/modelRouterService.test.js.map +1 -0
  107. package/dist/src/routing/routingStrategy.d.ts +62 -0
  108. package/dist/src/routing/routingStrategy.js +7 -0
  109. package/dist/src/routing/routingStrategy.js.map +1 -0
  110. package/dist/src/routing/strategies/compositeStrategy.d.ts +26 -0
  111. package/dist/src/routing/strategies/compositeStrategy.js +67 -0
  112. package/dist/src/routing/strategies/compositeStrategy.js.map +1 -0
  113. package/dist/src/routing/strategies/compositeStrategy.test.d.ts +6 -0
  114. package/dist/src/routing/strategies/compositeStrategy.test.js +123 -0
  115. package/dist/src/routing/strategies/compositeStrategy.test.js.map +1 -0
  116. package/dist/src/routing/strategies/defaultStrategy.d.ts +12 -0
  117. package/dist/src/routing/strategies/defaultStrategy.js +20 -0
  118. package/dist/src/routing/strategies/defaultStrategy.js.map +1 -0
  119. package/dist/src/routing/strategies/defaultStrategy.test.d.ts +6 -0
  120. package/dist/src/routing/strategies/defaultStrategy.test.js +26 -0
  121. package/dist/src/routing/strategies/defaultStrategy.test.js.map +1 -0
  122. package/dist/src/routing/strategies/fallbackStrategy.d.ts +12 -0
  123. package/dist/src/routing/strategies/fallbackStrategy.js +25 -0
  124. package/dist/src/routing/strategies/fallbackStrategy.js.map +1 -0
  125. package/dist/src/routing/strategies/fallbackStrategy.test.d.ts +6 -0
  126. package/dist/src/routing/strategies/fallbackStrategy.test.js +55 -0
  127. package/dist/src/routing/strategies/fallbackStrategy.test.js.map +1 -0
  128. package/dist/src/routing/strategies/overrideStrategy.d.ts +15 -0
  129. package/dist/src/routing/strategies/overrideStrategy.js +27 -0
  130. package/dist/src/routing/strategies/overrideStrategy.js.map +1 -0
  131. package/dist/src/routing/strategies/overrideStrategy.test.d.ts +6 -0
  132. package/dist/src/routing/strategies/overrideStrategy.test.js +41 -0
  133. package/dist/src/routing/strategies/overrideStrategy.test.js.map +1 -0
  134. package/dist/src/services/fileDiscoveryService.d.ts +10 -0
  135. package/dist/src/services/fileDiscoveryService.js +31 -17
  136. package/dist/src/services/fileDiscoveryService.js.map +1 -1
  137. package/dist/src/services/loopDetectionService.d.ts +5 -0
  138. package/dist/src/services/loopDetectionService.js +13 -2
  139. package/dist/src/services/loopDetectionService.js.map +1 -1
  140. package/dist/src/services/loopDetectionService.test.js +15 -0
  141. package/dist/src/services/loopDetectionService.test.js.map +1 -1
  142. package/dist/src/services/shellExecutionService.d.ts +34 -2
  143. package/dist/src/services/shellExecutionService.js +177 -43
  144. package/dist/src/services/shellExecutionService.js.map +1 -1
  145. package/dist/src/services/shellExecutionService.test.js +153 -56
  146. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  147. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +4 -2
  148. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +31 -4
  149. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  150. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +6 -2
  151. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +14 -2
  152. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  153. package/dist/src/telemetry/index.d.ts +2 -2
  154. package/dist/src/telemetry/index.js +2 -2
  155. package/dist/src/telemetry/index.js.map +1 -1
  156. package/dist/src/telemetry/loggers.d.ts +2 -1
  157. package/dist/src/telemetry/loggers.js +20 -5
  158. package/dist/src/telemetry/loggers.js.map +1 -1
  159. package/dist/src/telemetry/loggers.test.js +50 -35
  160. package/dist/src/telemetry/loggers.test.js.map +1 -1
  161. package/dist/src/telemetry/metrics.d.ts +1 -1
  162. package/dist/src/telemetry/metrics.js +2 -2
  163. package/dist/src/telemetry/metrics.js.map +1 -1
  164. package/dist/src/telemetry/types.d.ts +23 -3
  165. package/dist/src/telemetry/types.js +25 -3
  166. package/dist/src/telemetry/types.js.map +1 -1
  167. package/dist/src/tools/edit.js +2 -3
  168. package/dist/src/tools/edit.js.map +1 -1
  169. package/dist/src/tools/edit.test.js +2 -8
  170. package/dist/src/tools/edit.test.js.map +1 -1
  171. package/dist/src/tools/glob.d.ts +5 -1
  172. package/dist/src/tools/glob.js +24 -17
  173. package/dist/src/tools/glob.js.map +1 -1
  174. package/dist/src/tools/glob.test.js +51 -0
  175. package/dist/src/tools/glob.test.js.map +1 -1
  176. package/dist/src/tools/ls.js +19 -32
  177. package/dist/src/tools/ls.js.map +1 -1
  178. package/dist/src/tools/ls.test.js +140 -280
  179. package/dist/src/tools/ls.test.js.map +1 -1
  180. package/dist/src/tools/read-many-files.d.ts +1 -1
  181. package/dist/src/tools/read-many-files.js +17 -49
  182. package/dist/src/tools/read-many-files.js.map +1 -1
  183. package/dist/src/tools/ripGrep.d.ts +4 -0
  184. package/dist/src/tools/ripGrep.js +11 -1
  185. package/dist/src/tools/ripGrep.js.map +1 -1
  186. package/dist/src/tools/ripGrep.test.js +51 -1
  187. package/dist/src/tools/ripGrep.test.js.map +1 -1
  188. package/dist/src/tools/shell.d.ts +12 -2
  189. package/dist/src/tools/shell.js +12 -16
  190. package/dist/src/tools/shell.js.map +1 -1
  191. package/dist/src/tools/shell.test.js +2 -33
  192. package/dist/src/tools/shell.test.js.map +1 -1
  193. package/dist/src/tools/smart-edit.js +2 -3
  194. package/dist/src/tools/smart-edit.js.map +1 -1
  195. package/dist/src/tools/smart-edit.test.js +2 -8
  196. package/dist/src/tools/smart-edit.test.js.map +1 -1
  197. package/dist/src/tools/tools.d.ts +6 -4
  198. package/dist/src/tools/tools.js +2 -2
  199. package/dist/src/tools/tools.js.map +1 -1
  200. package/dist/src/tools/write-file.js +2 -3
  201. package/dist/src/tools/write-file.js.map +1 -1
  202. package/dist/src/tools/write-file.test.js +78 -0
  203. package/dist/src/tools/write-file.test.js.map +1 -1
  204. package/dist/src/utils/bfsFileSearch.js +11 -5
  205. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  206. package/dist/src/utils/errors.d.ts +6 -0
  207. package/dist/src/utils/errors.js +10 -0
  208. package/dist/src/utils/errors.js.map +1 -1
  209. package/dist/src/utils/geminiIgnoreParser.d.ts +18 -0
  210. package/dist/src/utils/geminiIgnoreParser.js +61 -0
  211. package/dist/src/utils/geminiIgnoreParser.js.map +1 -0
  212. package/dist/src/utils/geminiIgnoreParser.test.d.ts +6 -0
  213. package/dist/src/utils/geminiIgnoreParser.test.js +50 -0
  214. package/dist/src/utils/geminiIgnoreParser.test.js.map +1 -0
  215. package/dist/src/utils/gitIgnoreParser.d.ts +3 -9
  216. package/dist/src/utils/gitIgnoreParser.js +60 -69
  217. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  218. package/dist/src/utils/gitIgnoreParser.test.js +18 -53
  219. package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
  220. package/dist/src/utils/terminalSerializer.d.ts +28 -0
  221. package/dist/src/utils/terminalSerializer.js +432 -0
  222. package/dist/src/utils/terminalSerializer.js.map +1 -0
  223. package/dist/src/utils/terminalSerializer.test.d.ts +6 -0
  224. package/dist/src/utils/terminalSerializer.test.js +176 -0
  225. package/dist/src/utils/terminalSerializer.test.js.map +1 -0
  226. package/dist/tsconfig.tsbuildinfo +1 -1
  227. package/package.json +1 -1
  228. package/dist/google-gemini-cli-core-0.5.0-preview.tgz +0 -0
  229. package/dist/src/utils/ide-trust.d.ts +0 -10
  230. package/dist/src/utils/ide-trust.js +0 -14
  231. package/dist/src/utils/ide-trust.js.map +0 -1
@@ -13,7 +13,7 @@ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
13
13
  import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
14
14
  import { setSimulate429 } from '../utils/testUtils.js';
15
15
  import { tokenLimit } from './tokenLimits.js';
16
- import { ideContext } from '../ide/ideContext.js';
16
+ import { ideContextStore } from '../ide/ideContext.js';
17
17
  import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
18
18
  // Mock fs module to prevent actual file system operations during tests
19
19
  const mockFileSystem = new Map();
@@ -108,22 +108,22 @@ describe('findIndexAfterFraction', () => {
108
108
  // 0: 66
109
109
  // 1: 66 + 68 = 134
110
110
  // 2: 134 + 66 = 200
111
- // 200 >= 166.5, so index is 2
112
- expect(findIndexAfterFraction(history, 0.5)).toBe(2);
111
+ // 200 >= 166.5, so index is 3
112
+ expect(findIndexAfterFraction(history, 0.5)).toBe(3);
113
113
  });
114
114
  it('should handle a fraction that results in the last index', () => {
115
115
  // 333 * 0.9 = 299.7
116
116
  // ...
117
117
  // 3: 200 + 68 = 268
118
118
  // 4: 268 + 65 = 333
119
- // 333 >= 299.7, so index is 4
120
- expect(findIndexAfterFraction(history, 0.9)).toBe(4);
119
+ // 333 >= 299.7, so index is 5
120
+ expect(findIndexAfterFraction(history, 0.9)).toBe(5);
121
121
  });
122
122
  it('should handle an empty history', () => {
123
123
  expect(findIndexAfterFraction([], 0.5)).toBe(0);
124
124
  });
125
125
  it('should handle a history with only one item', () => {
126
- expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(0);
126
+ expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(1);
127
127
  });
128
128
  it('should handle history with weird parts', () => {
129
129
  const historyWithEmptyParts = [
@@ -131,7 +131,7 @@ describe('findIndexAfterFraction', () => {
131
131
  { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
132
132
  { role: 'user', parts: [{ text: 'Message 2' }] },
133
133
  ];
134
- expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(1);
134
+ expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(2);
135
135
  });
136
136
  });
137
137
  describe('isThinkingSupported', () => {
@@ -176,7 +176,7 @@ describe('Gemini Client (client.ts)', () => {
176
176
  mockContentGenerator = {
177
177
  generateContent: mockGenerateContentFn,
178
178
  generateContentStream: vi.fn(),
179
- countTokens: vi.fn(),
179
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
180
180
  embedContent: vi.fn(),
181
181
  batchEmbedContents: vi.fn(),
182
182
  };
@@ -189,7 +189,6 @@ describe('Gemini Client (client.ts)', () => {
189
189
  };
190
190
  const fileService = new FileDiscoveryService('/test/dir');
191
191
  const contentGeneratorConfig = {
192
- model: 'test-model',
193
192
  apiKey: 'test-key',
194
193
  vertexai: false,
195
194
  authType: AuthType.USE_GEMINI,
@@ -222,6 +221,9 @@ describe('Gemini Client (client.ts)', () => {
222
221
  getDirectories: vi.fn().mockReturnValue(['/test/dir']),
223
222
  }),
224
223
  getGeminiClient: vi.fn(),
224
+ getModelRouterService: vi.fn().mockReturnValue({
225
+ route: vi.fn().mockResolvedValue({ model: 'default-routed-model' }),
226
+ }),
225
227
  isInFallbackMode: vi.fn().mockReturnValue(false),
226
228
  setFallbackMode: vi.fn(),
227
229
  getChatCompression: vi.fn().mockReturnValue(undefined),
@@ -400,7 +402,6 @@ describe('Gemini Client (client.ts)', () => {
400
402
  });
401
403
  });
402
404
  describe('tryCompressChat', () => {
403
- const mockSendMessage = vi.fn();
404
405
  const mockGetHistory = vi.fn();
405
406
  beforeEach(() => {
406
407
  vi.mock('./tokenLimits', () => ({
@@ -410,7 +411,6 @@ describe('Gemini Client (client.ts)', () => {
410
411
  getHistory: mockGetHistory,
411
412
  addHistory: vi.fn(),
412
413
  setHistory: vi.fn(),
413
- sendMessage: mockSendMessage,
414
414
  };
415
415
  });
416
416
  function setup({ chatHistory = [
@@ -420,7 +420,6 @@ describe('Gemini Client (client.ts)', () => {
420
420
  const mockChat = {
421
421
  getHistory: vi.fn().mockReturnValue(chatHistory),
422
422
  setHistory: vi.fn(),
423
- sendMessage: vi.fn().mockResolvedValue({ text: 'Summary' }),
424
423
  };
425
424
  vi.mocked(mockContentGenerator.countTokens)
426
425
  .mockResolvedValueOnce({ totalTokens: 1000 })
@@ -527,9 +526,15 @@ describe('Gemini Client (client.ts)', () => {
527
526
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
528
527
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
529
528
  // Mock the summary response from the chat
530
- mockSendMessage.mockResolvedValue({
531
- role: 'model',
532
- parts: [{ text: 'This is a summary.' }],
529
+ mockGenerateContentFn.mockResolvedValue({
530
+ candidates: [
531
+ {
532
+ content: {
533
+ role: 'model',
534
+ parts: [{ text: 'This is a summary.' }],
535
+ },
536
+ },
537
+ ],
533
538
  });
534
539
  await client.tryCompressChat('prompt-id-3');
535
540
  expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
@@ -553,15 +558,21 @@ describe('Gemini Client (client.ts)', () => {
553
558
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
554
559
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
555
560
  // Mock the summary response from the chat
556
- mockSendMessage.mockResolvedValue({
557
- role: 'model',
558
- parts: [{ text: 'This is a summary.' }],
561
+ mockGenerateContentFn.mockResolvedValue({
562
+ candidates: [
563
+ {
564
+ content: {
565
+ role: 'model',
566
+ parts: [{ text: 'This is a summary.' }],
567
+ },
568
+ },
569
+ ],
559
570
  });
560
571
  const initialChat = client.getChat();
561
572
  const result = await client.tryCompressChat('prompt-id-3');
562
573
  const newChat = client.getChat();
563
574
  expect(tokenLimit).toHaveBeenCalled();
564
- expect(mockSendMessage).toHaveBeenCalled();
575
+ expect(mockGenerateContentFn).toHaveBeenCalled();
565
576
  // Assert that summarization happened and returned the correct stats
566
577
  expect(result).toEqual({
567
578
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -598,15 +609,21 @@ describe('Gemini Client (client.ts)', () => {
598
609
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
599
610
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
600
611
  // Mock the summary response from the chat
601
- mockSendMessage.mockResolvedValue({
602
- role: 'model',
603
- parts: [{ text: 'This is a summary.' }],
612
+ mockGenerateContentFn.mockResolvedValue({
613
+ candidates: [
614
+ {
615
+ content: {
616
+ role: 'model',
617
+ parts: [{ text: 'This is a summary.' }],
618
+ },
619
+ },
620
+ ],
604
621
  });
605
622
  const initialChat = client.getChat();
606
623
  const result = await client.tryCompressChat('prompt-id-3');
607
624
  const newChat = client.getChat();
608
625
  expect(tokenLimit).toHaveBeenCalled();
609
- expect(mockSendMessage).toHaveBeenCalled();
626
+ expect(mockGenerateContentFn).toHaveBeenCalled();
610
627
  // Assert that summarization happened and returned the correct stats
611
628
  expect(result).toEqual({
612
629
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -632,14 +649,20 @@ describe('Gemini Client (client.ts)', () => {
632
649
  .mockResolvedValueOnce({ totalTokens: originalTokenCount })
633
650
  .mockResolvedValueOnce({ totalTokens: newTokenCount });
634
651
  // Mock the summary response from the chat
635
- mockSendMessage.mockResolvedValue({
636
- role: 'model',
637
- parts: [{ text: 'This is a summary.' }],
652
+ mockGenerateContentFn.mockResolvedValue({
653
+ candidates: [
654
+ {
655
+ content: {
656
+ role: 'model',
657
+ parts: [{ text: 'This is a summary.' }],
658
+ },
659
+ },
660
+ ],
638
661
  });
639
662
  const initialChat = client.getChat();
640
663
  const result = await client.tryCompressChat('prompt-id-1', true); // force = true
641
664
  const newChat = client.getChat();
642
- expect(mockSendMessage).toHaveBeenCalled();
665
+ expect(mockGenerateContentFn).toHaveBeenCalled();
643
666
  expect(result).toEqual({
644
667
  compressionStatus: CompressionStatus.COMPRESSED,
645
668
  originalTokenCount,
@@ -740,7 +763,7 @@ describe('Gemini Client (client.ts)', () => {
740
763
  });
741
764
  it('should include editor context when ideMode is enabled', async () => {
742
765
  // Arrange
743
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
766
+ vi.mocked(ideContextStore.get).mockReturnValue({
744
767
  workspaceState: {
745
768
  openFiles: [
746
769
  {
@@ -777,7 +800,7 @@ describe('Gemini Client (client.ts)', () => {
777
800
  // consume stream
778
801
  }
779
802
  // Assert
780
- expect(ideContext.getIdeContext).toHaveBeenCalled();
803
+ expect(ideContextStore.get).toHaveBeenCalled();
781
804
  const expectedContext = `
782
805
  Here is the user's editor context as a JSON object. This is for your information only.
783
806
  \`\`\`json
@@ -802,7 +825,7 @@ ${JSON.stringify({
802
825
  });
803
826
  it('should not add context if ideMode is enabled but no open files', async () => {
804
827
  // Arrange
805
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
828
+ vi.mocked(ideContextStore.get).mockReturnValue({
806
829
  workspaceState: {
807
830
  openFiles: [],
808
831
  },
@@ -824,12 +847,16 @@ ${JSON.stringify({
824
847
  // consume stream
825
848
  }
826
849
  // Assert
827
- expect(ideContext.getIdeContext).toHaveBeenCalled();
828
- expect(mockTurnRunFn).toHaveBeenCalledWith(initialRequest, expect.any(Object));
850
+ expect(ideContextStore.get).toHaveBeenCalled();
851
+ // The `turn.run` method is now called with the model name as the first
852
+ // argument. We use `expect.any(String)` because this test is
853
+ // concerned with the IDE context logic, not the model routing,
854
+ // which is tested in its own dedicated suite.
855
+ expect(mockTurnRunFn).toHaveBeenCalledWith(expect.any(String), initialRequest, expect.any(Object));
829
856
  });
830
857
  it('should add context if ideMode is enabled and there is one active file', async () => {
831
858
  // Arrange
832
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
859
+ vi.mocked(ideContextStore.get).mockReturnValue({
833
860
  workspaceState: {
834
861
  openFiles: [
835
862
  {
@@ -859,7 +886,7 @@ ${JSON.stringify({
859
886
  // consume stream
860
887
  }
861
888
  // Assert
862
- expect(ideContext.getIdeContext).toHaveBeenCalled();
889
+ expect(ideContextStore.get).toHaveBeenCalled();
863
890
  const expectedContext = `
864
891
  Here is the user's editor context as a JSON object. This is for your information only.
865
892
  \`\`\`json
@@ -883,7 +910,7 @@ ${JSON.stringify({
883
910
  });
884
911
  it('should add context if ideMode is enabled and there are open files but no active file', async () => {
885
912
  // Arrange
886
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
913
+ vi.mocked(ideContextStore.get).mockReturnValue({
887
914
  workspaceState: {
888
915
  openFiles: [
889
916
  {
@@ -914,7 +941,7 @@ ${JSON.stringify({
914
941
  // consume stream
915
942
  }
916
943
  // Assert
917
- expect(ideContext.getIdeContext).toHaveBeenCalled();
944
+ expect(ideContextStore.get).toHaveBeenCalled();
918
945
  const expectedContext = `
919
946
  Here is the user's editor context as a JSON object. This is for your information only.
920
947
  \`\`\`json
@@ -1110,6 +1137,91 @@ ${JSON.stringify({
1110
1137
  console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
1111
1138
  `${eventCount} events generated (properly bounded by MAX_TURNS)`);
1112
1139
  });
1140
+ describe('Model Routing', () => {
1141
+ let mockRouterService;
1142
+ beforeEach(() => {
1143
+ mockRouterService = {
1144
+ route: vi
1145
+ .fn()
1146
+ .mockResolvedValue({ model: 'routed-model', reason: 'test' }),
1147
+ };
1148
+ vi.mocked(mockConfig.getModelRouterService).mockReturnValue(mockRouterService);
1149
+ mockTurnRunFn.mockReturnValue((async function* () {
1150
+ yield { type: 'content', value: 'Hello' };
1151
+ })());
1152
+ });
1153
+ it('should use the model router service to select a model on the first turn', async () => {
1154
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1155
+ await fromAsync(stream); // consume stream
1156
+ expect(mockConfig.getModelRouterService).toHaveBeenCalled();
1157
+ expect(mockRouterService.route).toHaveBeenCalled();
1158
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', // The model from the router
1159
+ [{ text: 'Hi' }], expect.any(Object));
1160
+ });
1161
+ it('should use the same model for subsequent turns in the same prompt (stickiness)', async () => {
1162
+ // First turn
1163
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1164
+ await fromAsync(stream);
1165
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1166
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1167
+ // Second turn
1168
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-1');
1169
+ await fromAsync(stream);
1170
+ // Router should not be called again
1171
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1172
+ // Should stick to the first model
1173
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Continue' }], expect.any(Object));
1174
+ });
1175
+ it('should reset the sticky model and re-route when the prompt_id changes', async () => {
1176
+ // First prompt
1177
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1178
+ await fromAsync(stream);
1179
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1180
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1181
+ // New prompt
1182
+ mockRouterService.route.mockResolvedValue({
1183
+ model: 'new-routed-model',
1184
+ reason: 'test',
1185
+ });
1186
+ stream = client.sendMessageStream([{ text: 'A new topic' }], new AbortController().signal, 'prompt-2');
1187
+ await fromAsync(stream);
1188
+ // Router should be called again for the new prompt
1189
+ expect(mockRouterService.route).toHaveBeenCalledTimes(2);
1190
+ // Should use the newly routed model
1191
+ expect(mockTurnRunFn).toHaveBeenCalledWith('new-routed-model', [{ text: 'A new topic' }], expect.any(Object));
1192
+ });
1193
+ it('should use the fallback model and bypass routing when in fallback mode', async () => {
1194
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1195
+ mockRouterService.route.mockResolvedValue({
1196
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1197
+ reason: 'fallback',
1198
+ });
1199
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1200
+ await fromAsync(stream);
1201
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1202
+ });
1203
+ it('should stick to the fallback model for the entire sequence even if fallback mode ends', async () => {
1204
+ // Start the sequence in fallback mode
1205
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1206
+ mockRouterService.route.mockResolvedValue({
1207
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1208
+ reason: 'fallback',
1209
+ });
1210
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-fallback-stickiness');
1211
+ await fromAsync(stream);
1212
+ // First call should use fallback model
1213
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1214
+ // End fallback mode
1215
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(false);
1216
+ // Second call in the same sequence
1217
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-fallback-stickiness');
1218
+ await fromAsync(stream);
1219
+ // Router should still not be called, and it should stick to the fallback model
1220
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(2); // Ensure it was called again
1221
+ expect(mockTurnRunFn).toHaveBeenLastCalledWith(DEFAULT_GEMINI_FLASH_MODEL, // Still the fallback model
1222
+ [{ text: 'Continue' }], expect.any(Object));
1223
+ });
1224
+ });
1113
1225
  describe('Editor context delta', () => {
1114
1226
  const mockStream = (async function* () {
1115
1227
  yield { type: 'content', value: 'Hello' };
@@ -1126,7 +1238,6 @@ ${JSON.stringify({
1126
1238
  const mockChat = {
1127
1239
  addHistory: vi.fn(),
1128
1240
  setHistory: vi.fn(),
1129
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1130
1241
  // Assume history is not empty for delta checks
1131
1242
  getHistory: vi
1132
1243
  .fn()
@@ -1250,7 +1361,7 @@ ${JSON.stringify({
1250
1361
  },
1251
1362
  };
1252
1363
  // Setup current context
1253
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1364
+ vi.mocked(ideContextStore.get).mockReturnValue({
1254
1365
  workspaceState: {
1255
1366
  openFiles: [
1256
1367
  { ...currentActiveFile, isActive: true, timestamp: Date.now() },
@@ -1296,7 +1407,7 @@ ${JSON.stringify({
1296
1407
  },
1297
1408
  };
1298
1409
  // Setup current context (same as previous)
1299
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1410
+ vi.mocked(ideContextStore.get).mockReturnValue({
1300
1411
  workspaceState: {
1301
1412
  openFiles: [
1302
1413
  { ...activeFile, isActive: true, timestamp: Date.now() },
@@ -1341,11 +1452,10 @@ ${JSON.stringify({
1341
1452
  addHistory: vi.fn(),
1342
1453
  getHistory: vi.fn().mockReturnValue([]), // Default empty history
1343
1454
  setHistory: vi.fn(),
1344
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1345
1455
  };
1346
1456
  client['chat'] = mockChat;
1347
1457
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1348
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1458
+ vi.mocked(ideContextStore.get).mockReturnValue({
1349
1459
  workspaceState: {
1350
1460
  openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
1351
1461
  },
@@ -1421,7 +1531,7 @@ ${JSON.stringify({
1421
1531
  openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
1422
1532
  },
1423
1533
  };
1424
- vi.mocked(ideContext.getIdeContext).mockReturnValue(initialIdeContext);
1534
+ vi.mocked(ideContextStore.get).mockReturnValue(initialIdeContext);
1425
1535
  // Act: Send the tool response
1426
1536
  let stream = client.sendMessageStream([
1427
1537
  {
@@ -1467,7 +1577,7 @@ ${JSON.stringify({
1467
1577
  openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
1468
1578
  },
1469
1579
  };
1470
- vi.mocked(ideContext.getIdeContext).mockReturnValue(newIdeContext);
1580
+ vi.mocked(ideContextStore.get).mockReturnValue(newIdeContext);
1471
1581
  // Act: Send a new, regular user message
1472
1582
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1473
1583
  for await (const _ of stream) {
@@ -1497,7 +1607,7 @@ ${JSON.stringify({
1497
1607
  ],
1498
1608
  },
1499
1609
  };
1500
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextA);
1610
+ vi.mocked(ideContextStore.get).mockReturnValue(contextA);
1501
1611
  // Act: Send a regular message to establish the initial context
1502
1612
  let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
1503
1613
  for await (const _ of stream) {
@@ -1530,7 +1640,7 @@ ${JSON.stringify({
1530
1640
  ],
1531
1641
  },
1532
1642
  };
1533
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextB);
1643
+ vi.mocked(ideContextStore.get).mockReturnValue(contextB);
1534
1644
  // Act: Send the tool response
1535
1645
  stream = client.sendMessageStream([
1536
1646
  {
@@ -1575,7 +1685,7 @@ ${JSON.stringify({
1575
1685
  ],
1576
1686
  },
1577
1687
  };
1578
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextC);
1688
+ vi.mocked(ideContextStore.get).mockReturnValue(contextC);
1579
1689
  // Act: Send a new, regular user message
1580
1690
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1581
1691
  for await (const _ of stream) {