@google/gemini-cli-core 0.6.0-nightly.20250910.a31830a3 → 0.6.0-preview.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +12 -2
  3. package/dist/index.d.ts +4 -4
  4. package/dist/index.js +4 -4
  5. package/dist/index.js.map +1 -1
  6. package/dist/src/code_assist/converter.d.ts +1 -0
  7. package/dist/src/code_assist/converter.js +1 -0
  8. package/dist/src/code_assist/converter.js.map +1 -1
  9. package/dist/src/code_assist/converter.test.js +10 -0
  10. package/dist/src/code_assist/converter.test.js.map +1 -1
  11. package/dist/src/code_assist/oauth-credential-storage.d.ts +5 -7
  12. package/dist/src/code_assist/oauth-credential-storage.js +5 -8
  13. package/dist/src/code_assist/oauth-credential-storage.js.map +1 -1
  14. package/dist/src/code_assist/oauth-credential-storage.test.js +35 -33
  15. package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -1
  16. package/dist/src/code_assist/oauth2.js +28 -2
  17. package/dist/src/code_assist/oauth2.js.map +1 -1
  18. package/dist/src/code_assist/oauth2.test.js +674 -536
  19. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  20. package/dist/src/config/config.d.ts +37 -1
  21. package/dist/src/config/config.js +74 -17
  22. package/dist/src/config/config.js.map +1 -1
  23. package/dist/src/config/config.test.js +104 -16
  24. package/dist/src/config/config.test.js.map +1 -1
  25. package/dist/src/config/models.d.ts +15 -0
  26. package/dist/src/config/models.js +27 -0
  27. package/dist/src/config/models.js.map +1 -1
  28. package/dist/src/config/models.test.d.ts +6 -0
  29. package/dist/src/config/models.test.js +55 -0
  30. package/dist/src/config/models.test.js.map +1 -0
  31. package/dist/src/confirmation-bus/index.d.ts +7 -0
  32. package/dist/src/confirmation-bus/index.js +8 -0
  33. package/dist/src/confirmation-bus/index.js.map +1 -0
  34. package/dist/src/confirmation-bus/message-bus.d.ts +17 -0
  35. package/dist/src/confirmation-bus/message-bus.js +81 -0
  36. package/dist/src/confirmation-bus/message-bus.js.map +1 -0
  37. package/dist/src/confirmation-bus/message-bus.test.d.ts +6 -0
  38. package/dist/src/confirmation-bus/message-bus.test.js +164 -0
  39. package/dist/src/confirmation-bus/message-bus.test.js.map +1 -0
  40. package/dist/src/confirmation-bus/types.d.ts +38 -0
  41. package/dist/src/confirmation-bus/types.js +15 -0
  42. package/dist/src/confirmation-bus/types.js.map +1 -0
  43. package/dist/src/core/baseLlmClient.d.ts +1 -0
  44. package/dist/src/core/baseLlmClient.js +24 -0
  45. package/dist/src/core/baseLlmClient.js.map +1 -1
  46. package/dist/src/core/baseLlmClient.test.js +63 -0
  47. package/dist/src/core/baseLlmClient.test.js.map +1 -1
  48. package/dist/src/core/client.d.ts +4 -3
  49. package/dist/src/core/client.js +76 -141
  50. package/dist/src/core/client.js.map +1 -1
  51. package/dist/src/core/client.test.js +218 -220
  52. package/dist/src/core/client.test.js.map +1 -1
  53. package/dist/src/core/contentGenerator.d.ts +0 -1
  54. package/dist/src/core/contentGenerator.js +0 -4
  55. package/dist/src/core/contentGenerator.js.map +1 -1
  56. package/dist/src/core/contentGenerator.test.js +0 -3
  57. package/dist/src/core/contentGenerator.test.js.map +1 -1
  58. package/dist/src/core/coreToolScheduler.d.ts +4 -3
  59. package/dist/src/core/coreToolScheduler.js +42 -5
  60. package/dist/src/core/coreToolScheduler.js.map +1 -1
  61. package/dist/src/core/coreToolScheduler.test.js +43 -0
  62. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  63. package/dist/src/core/geminiChat.d.ts +13 -30
  64. package/dist/src/core/geminiChat.js +88 -230
  65. package/dist/src/core/geminiChat.js.map +1 -1
  66. package/dist/src/core/geminiChat.test.js +268 -489
  67. package/dist/src/core/geminiChat.test.js.map +1 -1
  68. package/dist/src/core/loggingContentGenerator.js +5 -5
  69. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  70. package/dist/src/core/nonInteractiveToolExecutor.test.js +49 -0
  71. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  72. package/dist/src/core/subagent.js +1 -1
  73. package/dist/src/core/subagent.js.map +1 -1
  74. package/dist/src/core/subagent.test.js +9 -8
  75. package/dist/src/core/subagent.test.js.map +1 -1
  76. package/dist/src/core/turn.d.ts +2 -1
  77. package/dist/src/core/turn.js +2 -2
  78. package/dist/src/core/turn.js.map +1 -1
  79. package/dist/src/core/turn.test.js +18 -18
  80. package/dist/src/core/turn.test.js.map +1 -1
  81. package/dist/src/generated/git-commit.d.ts +2 -2
  82. package/dist/src/generated/git-commit.js +2 -2
  83. package/dist/src/generated/git-commit.js.map +1 -1
  84. package/dist/src/ide/constants.d.ts +1 -0
  85. package/dist/src/ide/constants.js +1 -0
  86. package/dist/src/ide/constants.js.map +1 -1
  87. package/dist/src/ide/ide-client.d.ts +51 -13
  88. package/dist/src/ide/ide-client.js +184 -37
  89. package/dist/src/ide/ide-client.js.map +1 -1
  90. package/dist/src/ide/ide-client.test.js +93 -3
  91. package/dist/src/ide/ide-client.test.js.map +1 -1
  92. package/dist/src/ide/ide-installer.js +8 -2
  93. package/dist/src/ide/ide-installer.js.map +1 -1
  94. package/dist/src/ide/ide-installer.test.js +13 -2
  95. package/dist/src/ide/ide-installer.test.js.map +1 -1
  96. package/dist/src/ide/ideContext.d.ts +34 -113
  97. package/dist/src/ide/ideContext.js +20 -78
  98. package/dist/src/ide/ideContext.js.map +1 -1
  99. package/dist/src/ide/ideContext.test.js +37 -39
  100. package/dist/src/ide/ideContext.test.js.map +1 -1
  101. package/dist/src/ide/types.d.ts +141 -0
  102. package/dist/src/ide/types.js +73 -0
  103. package/dist/src/ide/types.js.map +1 -1
  104. package/dist/src/index.d.ts +3 -1
  105. package/dist/src/index.js +3 -1
  106. package/dist/src/index.js.map +1 -1
  107. package/dist/src/mcp/oauth-token-storage.d.ts +2 -0
  108. package/dist/src/mcp/oauth-token-storage.js +25 -0
  109. package/dist/src/mcp/oauth-token-storage.js.map +1 -1
  110. package/dist/src/mcp/oauth-token-storage.test.js +251 -160
  111. package/dist/src/mcp/oauth-token-storage.test.js.map +1 -1
  112. package/dist/src/mcp/token-storage/index.d.ts +11 -0
  113. package/dist/src/mcp/token-storage/index.js +12 -0
  114. package/dist/src/mcp/token-storage/index.js.map +1 -0
  115. package/dist/src/output/json-formatter.d.ts +11 -0
  116. package/dist/src/output/json-formatter.js +30 -0
  117. package/dist/src/output/json-formatter.js.map +1 -0
  118. package/dist/src/output/json-formatter.test.d.ts +6 -0
  119. package/dist/src/output/json-formatter.test.js +266 -0
  120. package/dist/src/output/json-formatter.test.js.map +1 -0
  121. package/dist/src/output/types.d.ts +20 -0
  122. package/dist/src/output/types.js +11 -0
  123. package/dist/src/output/types.js.map +1 -0
  124. package/dist/src/policy/index.d.ts +7 -0
  125. package/dist/src/policy/index.js +8 -0
  126. package/dist/src/policy/index.js.map +1 -0
  127. package/dist/src/policy/policy-engine.d.ts +30 -0
  128. package/dist/src/policy/policy-engine.js +83 -0
  129. package/dist/src/policy/policy-engine.js.map +1 -0
  130. package/dist/src/policy/policy-engine.test.d.ts +6 -0
  131. package/dist/src/policy/policy-engine.test.js +470 -0
  132. package/dist/src/policy/policy-engine.test.js.map +1 -0
  133. package/dist/src/policy/stable-stringify.d.ts +58 -0
  134. package/dist/src/policy/stable-stringify.js +122 -0
  135. package/dist/src/policy/stable-stringify.js.map +1 -0
  136. package/dist/src/policy/types.d.ts +47 -0
  137. package/dist/src/policy/types.js +12 -0
  138. package/dist/src/policy/types.js.map +1 -0
  139. package/dist/src/routing/modelRouterService.d.ts +23 -0
  140. package/dist/src/routing/modelRouterService.js +70 -0
  141. package/dist/src/routing/modelRouterService.js.map +1 -0
  142. package/dist/src/routing/modelRouterService.test.d.ts +6 -0
  143. package/dist/src/routing/modelRouterService.test.js +98 -0
  144. package/dist/src/routing/modelRouterService.test.js.map +1 -0
  145. package/dist/src/routing/routingStrategy.d.ts +62 -0
  146. package/dist/src/routing/routingStrategy.js +7 -0
  147. package/dist/src/routing/routingStrategy.js.map +1 -0
  148. package/dist/src/routing/strategies/classifierStrategy.d.ts +12 -0
  149. package/dist/src/routing/strategies/classifierStrategy.js +173 -0
  150. package/dist/src/routing/strategies/classifierStrategy.js.map +1 -0
  151. package/dist/src/routing/strategies/classifierStrategy.test.d.ts +6 -0
  152. package/dist/src/routing/strategies/classifierStrategy.test.js +192 -0
  153. package/dist/src/routing/strategies/classifierStrategy.test.js.map +1 -0
  154. package/dist/src/routing/strategies/compositeStrategy.d.ts +26 -0
  155. package/dist/src/routing/strategies/compositeStrategy.js +67 -0
  156. package/dist/src/routing/strategies/compositeStrategy.js.map +1 -0
  157. package/dist/src/routing/strategies/compositeStrategy.test.d.ts +6 -0
  158. package/dist/src/routing/strategies/compositeStrategy.test.js +123 -0
  159. package/dist/src/routing/strategies/compositeStrategy.test.js.map +1 -0
  160. package/dist/src/routing/strategies/defaultStrategy.d.ts +12 -0
  161. package/dist/src/routing/strategies/defaultStrategy.js +20 -0
  162. package/dist/src/routing/strategies/defaultStrategy.js.map +1 -0
  163. package/dist/src/routing/strategies/defaultStrategy.test.d.ts +6 -0
  164. package/dist/src/routing/strategies/defaultStrategy.test.js +26 -0
  165. package/dist/src/routing/strategies/defaultStrategy.test.js.map +1 -0
  166. package/dist/src/routing/strategies/fallbackStrategy.d.ts +12 -0
  167. package/dist/src/routing/strategies/fallbackStrategy.js +25 -0
  168. package/dist/src/routing/strategies/fallbackStrategy.js.map +1 -0
  169. package/dist/src/routing/strategies/fallbackStrategy.test.d.ts +6 -0
  170. package/dist/src/routing/strategies/fallbackStrategy.test.js +55 -0
  171. package/dist/src/routing/strategies/fallbackStrategy.test.js.map +1 -0
  172. package/dist/src/routing/strategies/overrideStrategy.d.ts +15 -0
  173. package/dist/src/routing/strategies/overrideStrategy.js +28 -0
  174. package/dist/src/routing/strategies/overrideStrategy.js.map +1 -0
  175. package/dist/src/routing/strategies/overrideStrategy.test.d.ts +6 -0
  176. package/dist/src/routing/strategies/overrideStrategy.test.js +42 -0
  177. package/dist/src/routing/strategies/overrideStrategy.test.js.map +1 -0
  178. package/dist/src/services/chatRecordingService.d.ts +2 -1
  179. package/dist/src/services/chatRecordingService.js +3 -3
  180. package/dist/src/services/chatRecordingService.js.map +1 -1
  181. package/dist/src/services/chatRecordingService.test.js +8 -3
  182. package/dist/src/services/chatRecordingService.test.js.map +1 -1
  183. package/dist/src/services/fileDiscoveryService.d.ts +10 -0
  184. package/dist/src/services/fileDiscoveryService.js +31 -17
  185. package/dist/src/services/fileDiscoveryService.js.map +1 -1
  186. package/dist/src/services/gitService.js +9 -12
  187. package/dist/src/services/gitService.js.map +1 -1
  188. package/dist/src/services/gitService.test.js +10 -20
  189. package/dist/src/services/gitService.test.js.map +1 -1
  190. package/dist/src/services/loopDetectionService.d.ts +5 -0
  191. package/dist/src/services/loopDetectionService.js +36 -20
  192. package/dist/src/services/loopDetectionService.js.map +1 -1
  193. package/dist/src/services/loopDetectionService.test.js +41 -12
  194. package/dist/src/services/loopDetectionService.test.js.map +1 -1
  195. package/dist/src/services/shellExecutionService.d.ts +34 -2
  196. package/dist/src/services/shellExecutionService.js +192 -43
  197. package/dist/src/services/shellExecutionService.js.map +1 -1
  198. package/dist/src/services/shellExecutionService.test.js +184 -55
  199. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  200. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +12 -2
  201. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +100 -5
  202. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  203. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +63 -5
  204. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  205. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +13 -2
  206. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +33 -2
  207. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  208. package/dist/src/telemetry/constants.d.ts +6 -0
  209. package/dist/src/telemetry/constants.js +6 -0
  210. package/dist/src/telemetry/constants.js.map +1 -1
  211. package/dist/src/telemetry/gcp-exporters.d.ts +34 -0
  212. package/dist/src/telemetry/gcp-exporters.js +117 -0
  213. package/dist/src/telemetry/gcp-exporters.js.map +1 -0
  214. package/dist/src/telemetry/gcp-exporters.test.d.ts +6 -0
  215. package/dist/src/telemetry/gcp-exporters.test.js +318 -0
  216. package/dist/src/telemetry/gcp-exporters.test.js.map +1 -0
  217. package/dist/src/telemetry/index.d.ts +3 -2
  218. package/dist/src/telemetry/index.js +3 -2
  219. package/dist/src/telemetry/index.js.map +1 -1
  220. package/dist/src/telemetry/loggers.d.ts +7 -1
  221. package/dist/src/telemetry/loggers.js +97 -7
  222. package/dist/src/telemetry/loggers.js.map +1 -1
  223. package/dist/src/telemetry/loggers.test.js +171 -37
  224. package/dist/src/telemetry/loggers.test.js.map +1 -1
  225. package/dist/src/telemetry/metrics.d.ts +3 -1
  226. package/dist/src/telemetry/metrics.js +32 -3
  227. package/dist/src/telemetry/metrics.js.map +1 -1
  228. package/dist/src/telemetry/metrics.test.js +42 -0
  229. package/dist/src/telemetry/metrics.test.js.map +1 -1
  230. package/dist/src/telemetry/sdk.js +19 -1
  231. package/dist/src/telemetry/sdk.js.map +1 -1
  232. package/dist/src/telemetry/sdk.test.js +95 -0
  233. package/dist/src/telemetry/sdk.test.js.map +1 -1
  234. package/dist/src/telemetry/types.d.ts +54 -3
  235. package/dist/src/telemetry/types.js +79 -3
  236. package/dist/src/telemetry/types.js.map +1 -1
  237. package/dist/src/tools/edit.js +6 -5
  238. package/dist/src/tools/edit.js.map +1 -1
  239. package/dist/src/tools/edit.test.js +79 -9
  240. package/dist/src/tools/edit.test.js.map +1 -1
  241. package/dist/src/tools/glob.d.ts +5 -1
  242. package/dist/src/tools/glob.js +24 -17
  243. package/dist/src/tools/glob.js.map +1 -1
  244. package/dist/src/tools/glob.test.js +51 -0
  245. package/dist/src/tools/glob.test.js.map +1 -1
  246. package/dist/src/tools/ls.js +19 -32
  247. package/dist/src/tools/ls.js.map +1 -1
  248. package/dist/src/tools/ls.test.js +140 -280
  249. package/dist/src/tools/ls.test.js.map +1 -1
  250. package/dist/src/tools/read-many-files.d.ts +1 -1
  251. package/dist/src/tools/read-many-files.js +17 -49
  252. package/dist/src/tools/read-many-files.js.map +1 -1
  253. package/dist/src/tools/ripGrep.d.ts +4 -0
  254. package/dist/src/tools/ripGrep.js +11 -1
  255. package/dist/src/tools/ripGrep.js.map +1 -1
  256. package/dist/src/tools/ripGrep.test.js +51 -1
  257. package/dist/src/tools/ripGrep.test.js.map +1 -1
  258. package/dist/src/tools/shell.d.ts +12 -2
  259. package/dist/src/tools/shell.js +20 -27
  260. package/dist/src/tools/shell.js.map +1 -1
  261. package/dist/src/tools/shell.test.js +33 -68
  262. package/dist/src/tools/shell.test.js.map +1 -1
  263. package/dist/src/tools/smart-edit.d.ts +0 -1
  264. package/dist/src/tools/smart-edit.js +5 -18
  265. package/dist/src/tools/smart-edit.js.map +1 -1
  266. package/dist/src/tools/smart-edit.test.js +18 -9
  267. package/dist/src/tools/smart-edit.test.js.map +1 -1
  268. package/dist/src/tools/tool-registry.js +1 -0
  269. package/dist/src/tools/tool-registry.js.map +1 -1
  270. package/dist/src/tools/tools.d.ts +8 -5
  271. package/dist/src/tools/tools.js +9 -2
  272. package/dist/src/tools/tools.js.map +1 -1
  273. package/dist/src/tools/write-file.js +4 -5
  274. package/dist/src/tools/write-file.js.map +1 -1
  275. package/dist/src/tools/write-file.test.js +94 -10
  276. package/dist/src/tools/write-file.test.js.map +1 -1
  277. package/dist/src/utils/bfsFileSearch.js +11 -5
  278. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  279. package/dist/src/utils/editCorrector.d.ts +7 -6
  280. package/dist/src/utils/editCorrector.js +61 -18
  281. package/dist/src/utils/editCorrector.js.map +1 -1
  282. package/dist/src/utils/editCorrector.test.js +30 -79
  283. package/dist/src/utils/editCorrector.test.js.map +1 -1
  284. package/dist/src/utils/editor.js +31 -44
  285. package/dist/src/utils/editor.js.map +1 -1
  286. package/dist/src/utils/editor.test.js +61 -75
  287. package/dist/src/utils/editor.test.js.map +1 -1
  288. package/dist/src/utils/errorParsing.js +2 -2
  289. package/dist/src/utils/errorParsing.js.map +1 -1
  290. package/dist/src/utils/errorParsing.test.js +7 -7
  291. package/dist/src/utils/errorParsing.test.js.map +1 -1
  292. package/dist/src/utils/errors.d.ts +6 -0
  293. package/dist/src/utils/errors.js +10 -0
  294. package/dist/src/utils/errors.js.map +1 -1
  295. package/dist/src/utils/fileUtils.test.js +17 -8
  296. package/dist/src/utils/fileUtils.test.js.map +1 -1
  297. package/dist/src/utils/geminiIgnoreParser.d.ts +18 -0
  298. package/dist/src/utils/geminiIgnoreParser.js +61 -0
  299. package/dist/src/utils/geminiIgnoreParser.js.map +1 -0
  300. package/dist/src/utils/geminiIgnoreParser.test.d.ts +6 -0
  301. package/dist/src/utils/geminiIgnoreParser.test.js +50 -0
  302. package/dist/src/utils/geminiIgnoreParser.test.js.map +1 -0
  303. package/dist/src/utils/gitIgnoreParser.d.ts +3 -9
  304. package/dist/src/utils/gitIgnoreParser.js +60 -69
  305. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  306. package/dist/src/utils/gitIgnoreParser.test.js +18 -53
  307. package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
  308. package/dist/src/utils/memoryDiscovery.test.js +12 -6
  309. package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
  310. package/dist/src/utils/nextSpeakerChecker.d.ts +2 -2
  311. package/dist/src/utils/nextSpeakerChecker.js +8 -2
  312. package/dist/src/utils/nextSpeakerChecker.js.map +1 -1
  313. package/dist/src/utils/nextSpeakerChecker.test.js +40 -33
  314. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
  315. package/dist/src/utils/shell-utils.d.ts +5 -0
  316. package/dist/src/utils/shell-utils.js +23 -0
  317. package/dist/src/utils/shell-utils.js.map +1 -1
  318. package/dist/src/utils/terminalSerializer.d.ts +28 -0
  319. package/dist/src/utils/terminalSerializer.js +432 -0
  320. package/dist/src/utils/terminalSerializer.js.map +1 -0
  321. package/dist/src/utils/terminalSerializer.test.d.ts +6 -0
  322. package/dist/src/utils/terminalSerializer.test.js +176 -0
  323. package/dist/src/utils/terminalSerializer.test.js.map +1 -0
  324. package/dist/src/utils/textUtils.d.ts +5 -0
  325. package/dist/src/utils/textUtils.js +14 -0
  326. package/dist/src/utils/textUtils.js.map +1 -1
  327. package/dist/src/utils/textUtils.test.d.ts +6 -0
  328. package/dist/src/utils/textUtils.test.js +59 -0
  329. package/dist/src/utils/textUtils.test.js.map +1 -0
  330. package/dist/tsconfig.tsbuildinfo +1 -1
  331. package/package.json +5 -1
  332. package/dist/src/utils/ide-trust.d.ts +0 -10
  333. package/dist/src/utils/ide-trust.js +0 -14
  334. 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,8 +176,7 @@ describe('Gemini Client (client.ts)', () => {
176
176
  mockContentGenerator = {
177
177
  generateContent: mockGenerateContentFn,
178
178
  generateContentStream: vi.fn(),
179
- countTokens: vi.fn(),
180
- embedContent: vi.fn(),
179
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
181
180
  batchEmbedContents: vi.fn(),
182
181
  };
183
182
  // Because the GeminiClient constructor kicks off an async process (startChat)
@@ -189,7 +188,6 @@ describe('Gemini Client (client.ts)', () => {
189
188
  };
190
189
  const fileService = new FileDiscoveryService('/test/dir');
191
190
  const contentGeneratorConfig = {
192
- model: 'test-model',
193
191
  apiKey: 'test-key',
194
192
  vertexai: false,
195
193
  authType: AuthType.USE_GEMINI,
@@ -222,16 +220,26 @@ describe('Gemini Client (client.ts)', () => {
222
220
  getDirectories: vi.fn().mockReturnValue(['/test/dir']),
223
221
  }),
224
222
  getGeminiClient: vi.fn(),
223
+ getModelRouterService: vi.fn().mockReturnValue({
224
+ route: vi.fn().mockResolvedValue({ model: 'default-routed-model' }),
225
+ }),
225
226
  isInFallbackMode: vi.fn().mockReturnValue(false),
226
227
  setFallbackMode: vi.fn(),
227
228
  getChatCompression: vi.fn().mockReturnValue(undefined),
228
229
  getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
229
230
  getUseSmartEdit: vi.fn().mockReturnValue(false),
231
+ getUseModelRouter: vi.fn().mockReturnValue(false),
230
232
  getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
231
233
  storage: {
232
234
  getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
233
235
  },
234
236
  getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
237
+ getBaseLlmClient: vi.fn().mockReturnValue({
238
+ generateJson: vi.fn().mockResolvedValue({
239
+ next_speaker: 'user',
240
+ reasoning: 'test',
241
+ }),
242
+ }),
235
243
  };
236
244
  client = new GeminiClient(mockConfig);
237
245
  await client.initialize();
@@ -240,129 +248,6 @@ describe('Gemini Client (client.ts)', () => {
240
248
  afterEach(() => {
241
249
  vi.restoreAllMocks();
242
250
  });
243
- describe('generateEmbedding', () => {
244
- const texts = ['hello world', 'goodbye world'];
245
- const testEmbeddingModel = 'test-embedding-model';
246
- it('should call embedContent with correct parameters and return embeddings', async () => {
247
- const mockEmbeddings = [
248
- [0.1, 0.2, 0.3],
249
- [0.4, 0.5, 0.6],
250
- ];
251
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
252
- embeddings: [
253
- { values: mockEmbeddings[0] },
254
- { values: mockEmbeddings[1] },
255
- ],
256
- });
257
- const result = await client.generateEmbedding(texts);
258
- expect(mockContentGenerator.embedContent).toHaveBeenCalledTimes(1);
259
- expect(mockContentGenerator.embedContent).toHaveBeenCalledWith({
260
- model: testEmbeddingModel,
261
- contents: texts,
262
- });
263
- expect(result).toEqual(mockEmbeddings);
264
- });
265
- it('should return an empty array if an empty array is passed', async () => {
266
- const result = await client.generateEmbedding([]);
267
- expect(result).toEqual([]);
268
- expect(mockContentGenerator.embedContent).not.toHaveBeenCalled();
269
- });
270
- it('should throw an error if API response has no embeddings array', async () => {
271
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({});
272
- await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
273
- });
274
- it('should throw an error if API response has an empty embeddings array', async () => {
275
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
276
- embeddings: [],
277
- });
278
- await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
279
- });
280
- it('should throw an error if API returns a mismatched number of embeddings', async () => {
281
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
282
- embeddings: [{ values: [1, 2, 3] }], // Only one for two texts
283
- });
284
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned a mismatched number of embeddings. Expected 2, got 1.');
285
- });
286
- it('should throw an error if any embedding has nullish values', async () => {
287
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
288
- embeddings: [{ values: [1, 2, 3] }, { values: undefined }], // Second one is bad
289
- });
290
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 1: "goodbye world"');
291
- });
292
- it('should throw an error if any embedding has an empty values array', async () => {
293
- vi.mocked(mockContentGenerator.embedContent).mockResolvedValue({
294
- embeddings: [{ values: [] }, { values: [1, 2, 3] }], // First one is bad
295
- });
296
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 0: "hello world"');
297
- });
298
- it('should propagate errors from the API call', async () => {
299
- vi.mocked(mockContentGenerator.embedContent).mockRejectedValue(new Error('API Failure'));
300
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API Failure');
301
- });
302
- });
303
- describe('generateJson', () => {
304
- it('should call generateContent with the correct parameters', async () => {
305
- const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
306
- const schema = { type: 'string' };
307
- const abortSignal = new AbortController().signal;
308
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
309
- totalTokens: 1,
310
- });
311
- await client.generateJson(contents, schema, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
312
- expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
313
- model: DEFAULT_GEMINI_FLASH_MODEL,
314
- config: {
315
- abortSignal,
316
- systemInstruction: getCoreSystemPrompt(''),
317
- temperature: 0,
318
- topP: 1,
319
- responseJsonSchema: schema,
320
- responseMimeType: 'application/json',
321
- },
322
- contents,
323
- }, 'test-session-id');
324
- });
325
- it('should allow overriding model and config', async () => {
326
- const contents = [
327
- { role: 'user', parts: [{ text: 'hello' }] },
328
- ];
329
- const schema = { type: 'string' };
330
- const abortSignal = new AbortController().signal;
331
- const customModel = 'custom-json-model';
332
- const customConfig = { temperature: 0.9, topK: 20 };
333
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
334
- totalTokens: 1,
335
- });
336
- await client.generateJson(contents, schema, abortSignal, customModel, customConfig);
337
- expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
338
- model: customModel,
339
- config: {
340
- abortSignal,
341
- systemInstruction: getCoreSystemPrompt(''),
342
- temperature: 0.9,
343
- topP: 1, // from default
344
- topK: 20,
345
- responseJsonSchema: schema,
346
- responseMimeType: 'application/json',
347
- },
348
- contents,
349
- }, 'test-session-id');
350
- });
351
- it('should use the Flash model when fallback mode is active', async () => {
352
- const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
353
- const schema = { type: 'string' };
354
- const abortSignal = new AbortController().signal;
355
- const requestedModel = 'gemini-2.5-pro'; // A non-flash model
356
- // Mock config to be in fallback mode
357
- // We access the mock via the client instance which holds the mocked config
358
- vi.spyOn(client['config'], 'isInFallbackMode').mockReturnValue(true);
359
- await client.generateJson(contents, schema, abortSignal, requestedModel);
360
- // Assert that the Flash model was used, not the requested model
361
- expect(mockContentGenerator.generateContent).toHaveBeenCalledWith(expect.objectContaining({
362
- model: DEFAULT_GEMINI_FLASH_MODEL,
363
- }), 'test-session-id');
364
- });
365
- });
366
251
  describe('addHistory', () => {
367
252
  it('should call chat.addHistory with the provided content', async () => {
368
253
  const mockChat = {
@@ -400,7 +285,6 @@ describe('Gemini Client (client.ts)', () => {
400
285
  });
401
286
  });
402
287
  describe('tryCompressChat', () => {
403
- const mockSendMessage = vi.fn();
404
288
  const mockGetHistory = vi.fn();
405
289
  beforeEach(() => {
406
290
  vi.mock('./tokenLimits', () => ({
@@ -410,7 +294,6 @@ describe('Gemini Client (client.ts)', () => {
410
294
  getHistory: mockGetHistory,
411
295
  addHistory: vi.fn(),
412
296
  setHistory: vi.fn(),
413
- sendMessage: mockSendMessage,
414
297
  };
415
298
  });
416
299
  function setup({ chatHistory = [
@@ -420,7 +303,6 @@ describe('Gemini Client (client.ts)', () => {
420
303
  const mockChat = {
421
304
  getHistory: vi.fn().mockReturnValue(chatHistory),
422
305
  setHistory: vi.fn(),
423
- sendMessage: vi.fn().mockResolvedValue({ text: 'Summary' }),
424
306
  };
425
307
  vi.mocked(mockContentGenerator.countTokens)
426
308
  .mockResolvedValueOnce({ totalTokens: 1000 })
@@ -435,7 +317,7 @@ describe('Gemini Client (client.ts)', () => {
435
317
  vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
436
318
  totalTokens: 1000,
437
319
  });
438
- await client.tryCompressChat('prompt-id-4'); // Fails
320
+ await client.tryCompressChat('prompt-id-4', false); // Fails
439
321
  const result = await client.tryCompressChat('prompt-id-4', true);
440
322
  expect(result).toEqual({
441
323
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -448,7 +330,7 @@ describe('Gemini Client (client.ts)', () => {
448
330
  vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
449
331
  totalTokens: 1000,
450
332
  });
451
- const result = await client.tryCompressChat('prompt-id-4', true);
333
+ const result = await client.tryCompressChat('prompt-id-4', false);
452
334
  expect(result).toEqual({
453
335
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
454
336
  newTokenCount: 5000,
@@ -457,7 +339,7 @@ describe('Gemini Client (client.ts)', () => {
457
339
  });
458
340
  it('does not manipulate the source chat', async () => {
459
341
  const { client, mockChat } = setup();
460
- await client.tryCompressChat('prompt-id-4', true);
342
+ await client.tryCompressChat('prompt-id-4', false);
461
343
  expect(client['chat']).toBe(mockChat); // a new chat session was not created
462
344
  });
463
345
  it('restores the history back to the original', async () => {
@@ -473,14 +355,14 @@ describe('Gemini Client (client.ts)', () => {
473
355
  const { client } = setup({
474
356
  chatHistory: originalHistory,
475
357
  });
476
- const { compressionStatus } = await client.tryCompressChat('prompt-id-4');
358
+ const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false);
477
359
  expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
478
360
  expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
479
361
  });
480
362
  it('will not attempt to compress context after a failure', async () => {
481
363
  const { client } = setup();
482
- await client.tryCompressChat('prompt-id-4');
483
- const result = await client.tryCompressChat('prompt-id-5');
364
+ await client.tryCompressChat('prompt-id-4', false);
365
+ const result = await client.tryCompressChat('prompt-id-5', false);
484
366
  // it counts tokens for {original, compressed} and then never again
485
367
  expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
486
368
  expect(result).toEqual({
@@ -500,7 +382,7 @@ describe('Gemini Client (client.ts)', () => {
500
382
  totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
501
383
  });
502
384
  const initialChat = client.getChat();
503
- const result = await client.tryCompressChat('prompt-id-2');
385
+ const result = await client.tryCompressChat('prompt-id-2', false);
504
386
  const newChat = client.getChat();
505
387
  expect(tokenLimit).toHaveBeenCalled();
506
388
  expect(result).toEqual({
@@ -527,11 +409,17 @@ describe('Gemini Client (client.ts)', () => {
527
409
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
528
410
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
529
411
  // Mock the summary response from the chat
530
- mockSendMessage.mockResolvedValue({
531
- role: 'model',
532
- parts: [{ text: 'This is a summary.' }],
412
+ mockGenerateContentFn.mockResolvedValue({
413
+ candidates: [
414
+ {
415
+ content: {
416
+ role: 'model',
417
+ parts: [{ text: 'This is a summary.' }],
418
+ },
419
+ },
420
+ ],
533
421
  });
534
- await client.tryCompressChat('prompt-id-3');
422
+ await client.tryCompressChat('prompt-id-3', false);
535
423
  expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
536
424
  tokens_before: originalTokenCount,
537
425
  tokens_after: newTokenCount,
@@ -553,15 +441,21 @@ describe('Gemini Client (client.ts)', () => {
553
441
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
554
442
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
555
443
  // Mock the summary response from the chat
556
- mockSendMessage.mockResolvedValue({
557
- role: 'model',
558
- parts: [{ text: 'This is a summary.' }],
444
+ mockGenerateContentFn.mockResolvedValue({
445
+ candidates: [
446
+ {
447
+ content: {
448
+ role: 'model',
449
+ parts: [{ text: 'This is a summary.' }],
450
+ },
451
+ },
452
+ ],
559
453
  });
560
454
  const initialChat = client.getChat();
561
- const result = await client.tryCompressChat('prompt-id-3');
455
+ const result = await client.tryCompressChat('prompt-id-3', false);
562
456
  const newChat = client.getChat();
563
457
  expect(tokenLimit).toHaveBeenCalled();
564
- expect(mockSendMessage).toHaveBeenCalled();
458
+ expect(mockGenerateContentFn).toHaveBeenCalled();
565
459
  // Assert that summarization happened and returned the correct stats
566
460
  expect(result).toEqual({
567
461
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -598,15 +492,21 @@ describe('Gemini Client (client.ts)', () => {
598
492
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
599
493
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
600
494
  // Mock the summary response from the chat
601
- mockSendMessage.mockResolvedValue({
602
- role: 'model',
603
- parts: [{ text: 'This is a summary.' }],
495
+ mockGenerateContentFn.mockResolvedValue({
496
+ candidates: [
497
+ {
498
+ content: {
499
+ role: 'model',
500
+ parts: [{ text: 'This is a summary.' }],
501
+ },
502
+ },
503
+ ],
604
504
  });
605
505
  const initialChat = client.getChat();
606
- const result = await client.tryCompressChat('prompt-id-3');
506
+ const result = await client.tryCompressChat('prompt-id-3', false);
607
507
  const newChat = client.getChat();
608
508
  expect(tokenLimit).toHaveBeenCalled();
609
- expect(mockSendMessage).toHaveBeenCalled();
509
+ expect(mockGenerateContentFn).toHaveBeenCalled();
610
510
  // Assert that summarization happened and returned the correct stats
611
511
  expect(result).toEqual({
612
512
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -632,14 +532,20 @@ describe('Gemini Client (client.ts)', () => {
632
532
  .mockResolvedValueOnce({ totalTokens: originalTokenCount })
633
533
  .mockResolvedValueOnce({ totalTokens: newTokenCount });
634
534
  // Mock the summary response from the chat
635
- mockSendMessage.mockResolvedValue({
636
- role: 'model',
637
- parts: [{ text: 'This is a summary.' }],
535
+ mockGenerateContentFn.mockResolvedValue({
536
+ candidates: [
537
+ {
538
+ content: {
539
+ role: 'model',
540
+ parts: [{ text: 'This is a summary.' }],
541
+ },
542
+ },
543
+ ],
638
544
  });
639
545
  const initialChat = client.getChat();
640
- const result = await client.tryCompressChat('prompt-id-1', true); // force = true
546
+ const result = await client.tryCompressChat('prompt-id-1', false); // force = true
641
547
  const newChat = client.getChat();
642
- expect(mockSendMessage).toHaveBeenCalled();
548
+ expect(mockGenerateContentFn).toHaveBeenCalled();
643
549
  expect(result).toEqual({
644
550
  compressionStatus: CompressionStatus.COMPRESSED,
645
551
  originalTokenCount,
@@ -648,45 +554,6 @@ describe('Gemini Client (client.ts)', () => {
648
554
  // Assert that the chat was reset
649
555
  expect(newChat).not.toBe(initialChat);
650
556
  });
651
- it('should use current model from config for token counting after sendMessage', async () => {
652
- const initialModel = mockConfig.getModel();
653
- // mock the model has been changed between calls of `countTokens`
654
- const firstCurrentModel = initialModel + '-changed-1';
655
- const secondCurrentModel = initialModel + '-changed-2';
656
- vi.mocked(mockConfig.getModel)
657
- .mockReturnValueOnce(firstCurrentModel)
658
- .mockReturnValueOnce(secondCurrentModel);
659
- vi.mocked(mockContentGenerator.countTokens)
660
- .mockResolvedValueOnce({ totalTokens: 100000 })
661
- .mockResolvedValueOnce({ totalTokens: 5000 });
662
- const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
663
- const mockChatHistory = [
664
- { role: 'user', parts: [{ text: 'Long conversation' }] },
665
- { role: 'model', parts: [{ text: 'Long response' }] },
666
- ];
667
- const mockChat = {
668
- getHistory: vi.fn().mockReturnValue(mockChatHistory),
669
- setHistory: vi.fn(),
670
- sendMessage: mockSendMessage,
671
- };
672
- client['chat'] = mockChat;
673
- client['startChat'] = vi.fn().mockResolvedValue(mockChat);
674
- const result = await client.tryCompressChat('prompt-id-4', true);
675
- expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
676
- expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(1, {
677
- model: firstCurrentModel,
678
- contents: mockChatHistory,
679
- });
680
- expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(2, {
681
- model: secondCurrentModel,
682
- contents: expect.any(Array),
683
- });
684
- expect(result).toEqual({
685
- compressionStatus: CompressionStatus.COMPRESSED,
686
- originalTokenCount: 100000,
687
- newTokenCount: 5000,
688
- });
689
- });
690
557
  });
691
558
  describe('sendMessageStream', () => {
692
559
  it('emits a compression event when the context was automatically compressed', async () => {
@@ -740,7 +607,7 @@ describe('Gemini Client (client.ts)', () => {
740
607
  });
741
608
  it('should include editor context when ideMode is enabled', async () => {
742
609
  // Arrange
743
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
610
+ vi.mocked(ideContextStore.get).mockReturnValue({
744
611
  workspaceState: {
745
612
  openFiles: [
746
613
  {
@@ -762,6 +629,11 @@ describe('Gemini Client (client.ts)', () => {
762
629
  },
763
630
  });
764
631
  vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
632
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
633
+ originalTokenCount: 0,
634
+ newTokenCount: 0,
635
+ compressionStatus: CompressionStatus.COMPRESSED,
636
+ });
765
637
  mockTurnRunFn.mockReturnValue((async function* () {
766
638
  yield { type: 'content', value: 'Hello' };
767
639
  })());
@@ -777,7 +649,7 @@ describe('Gemini Client (client.ts)', () => {
777
649
  // consume stream
778
650
  }
779
651
  // Assert
780
- expect(ideContext.getIdeContext).toHaveBeenCalled();
652
+ expect(ideContextStore.get).toHaveBeenCalled();
781
653
  const expectedContext = `
782
654
  Here is the user's editor context as a JSON object. This is for your information only.
783
655
  \`\`\`json
@@ -802,7 +674,7 @@ ${JSON.stringify({
802
674
  });
803
675
  it('should not add context if ideMode is enabled but no open files', async () => {
804
676
  // Arrange
805
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
677
+ vi.mocked(ideContextStore.get).mockReturnValue({
806
678
  workspaceState: {
807
679
  openFiles: [],
808
680
  },
@@ -824,12 +696,16 @@ ${JSON.stringify({
824
696
  // consume stream
825
697
  }
826
698
  // Assert
827
- expect(ideContext.getIdeContext).toHaveBeenCalled();
828
- expect(mockTurnRunFn).toHaveBeenCalledWith(initialRequest, expect.any(Object));
699
+ expect(ideContextStore.get).toHaveBeenCalled();
700
+ // The `turn.run` method is now called with the model name as the first
701
+ // argument. We use `expect.any(String)` because this test is
702
+ // concerned with the IDE context logic, not the model routing,
703
+ // which is tested in its own dedicated suite.
704
+ expect(mockTurnRunFn).toHaveBeenCalledWith(expect.any(String), initialRequest, expect.any(Object));
829
705
  });
830
706
  it('should add context if ideMode is enabled and there is one active file', async () => {
831
707
  // Arrange
832
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
708
+ vi.mocked(ideContextStore.get).mockReturnValue({
833
709
  workspaceState: {
834
710
  openFiles: [
835
711
  {
@@ -843,6 +719,11 @@ ${JSON.stringify({
843
719
  },
844
720
  });
845
721
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
722
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
723
+ originalTokenCount: 0,
724
+ newTokenCount: 0,
725
+ compressionStatus: CompressionStatus.COMPRESSED,
726
+ });
846
727
  const mockStream = (async function* () {
847
728
  yield { type: 'content', value: 'Hello' };
848
729
  })();
@@ -859,7 +740,7 @@ ${JSON.stringify({
859
740
  // consume stream
860
741
  }
861
742
  // Assert
862
- expect(ideContext.getIdeContext).toHaveBeenCalled();
743
+ expect(ideContextStore.get).toHaveBeenCalled();
863
744
  const expectedContext = `
864
745
  Here is the user's editor context as a JSON object. This is for your information only.
865
746
  \`\`\`json
@@ -883,7 +764,7 @@ ${JSON.stringify({
883
764
  });
884
765
  it('should add context if ideMode is enabled and there are open files but no active file', async () => {
885
766
  // Arrange
886
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
767
+ vi.mocked(ideContextStore.get).mockReturnValue({
887
768
  workspaceState: {
888
769
  openFiles: [
889
770
  {
@@ -898,6 +779,11 @@ ${JSON.stringify({
898
779
  },
899
780
  });
900
781
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
782
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
783
+ originalTokenCount: 0,
784
+ newTokenCount: 0,
785
+ compressionStatus: CompressionStatus.COMPRESSED,
786
+ });
901
787
  const mockStream = (async function* () {
902
788
  yield { type: 'content', value: 'Hello' };
903
789
  })();
@@ -914,7 +800,7 @@ ${JSON.stringify({
914
800
  // consume stream
915
801
  }
916
802
  // Assert
917
- expect(ideContext.getIdeContext).toHaveBeenCalled();
803
+ expect(ideContextStore.get).toHaveBeenCalled();
918
804
  const expectedContext = `
919
805
  Here is the user's editor context as a JSON object. This is for your information only.
920
806
  \`\`\`json
@@ -1110,6 +996,91 @@ ${JSON.stringify({
1110
996
  console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
1111
997
  `${eventCount} events generated (properly bounded by MAX_TURNS)`);
1112
998
  });
999
+ describe('Model Routing', () => {
1000
+ let mockRouterService;
1001
+ beforeEach(() => {
1002
+ mockRouterService = {
1003
+ route: vi
1004
+ .fn()
1005
+ .mockResolvedValue({ model: 'routed-model', reason: 'test' }),
1006
+ };
1007
+ vi.mocked(mockConfig.getModelRouterService).mockReturnValue(mockRouterService);
1008
+ mockTurnRunFn.mockReturnValue((async function* () {
1009
+ yield { type: 'content', value: 'Hello' };
1010
+ })());
1011
+ });
1012
+ it('should use the model router service to select a model on the first turn', async () => {
1013
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1014
+ await fromAsync(stream); // consume stream
1015
+ expect(mockConfig.getModelRouterService).toHaveBeenCalled();
1016
+ expect(mockRouterService.route).toHaveBeenCalled();
1017
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', // The model from the router
1018
+ [{ text: 'Hi' }], expect.any(Object));
1019
+ });
1020
+ it('should use the same model for subsequent turns in the same prompt (stickiness)', async () => {
1021
+ // First turn
1022
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1023
+ await fromAsync(stream);
1024
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1025
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1026
+ // Second turn
1027
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-1');
1028
+ await fromAsync(stream);
1029
+ // Router should not be called again
1030
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1031
+ // Should stick to the first model
1032
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Continue' }], expect.any(Object));
1033
+ });
1034
+ it('should reset the sticky model and re-route when the prompt_id changes', async () => {
1035
+ // First prompt
1036
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1037
+ await fromAsync(stream);
1038
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1039
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1040
+ // New prompt
1041
+ mockRouterService.route.mockResolvedValue({
1042
+ model: 'new-routed-model',
1043
+ reason: 'test',
1044
+ });
1045
+ stream = client.sendMessageStream([{ text: 'A new topic' }], new AbortController().signal, 'prompt-2');
1046
+ await fromAsync(stream);
1047
+ // Router should be called again for the new prompt
1048
+ expect(mockRouterService.route).toHaveBeenCalledTimes(2);
1049
+ // Should use the newly routed model
1050
+ expect(mockTurnRunFn).toHaveBeenCalledWith('new-routed-model', [{ text: 'A new topic' }], expect.any(Object));
1051
+ });
1052
+ it('should use the fallback model and bypass routing when in fallback mode', async () => {
1053
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1054
+ mockRouterService.route.mockResolvedValue({
1055
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1056
+ reason: 'fallback',
1057
+ });
1058
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1059
+ await fromAsync(stream);
1060
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1061
+ });
1062
+ it('should stick to the fallback model for the entire sequence even if fallback mode ends', async () => {
1063
+ // Start the sequence in fallback mode
1064
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1065
+ mockRouterService.route.mockResolvedValue({
1066
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1067
+ reason: 'fallback',
1068
+ });
1069
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-fallback-stickiness');
1070
+ await fromAsync(stream);
1071
+ // First call should use fallback model
1072
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1073
+ // End fallback mode
1074
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(false);
1075
+ // Second call in the same sequence
1076
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-fallback-stickiness');
1077
+ await fromAsync(stream);
1078
+ // Router should still not be called, and it should stick to the fallback model
1079
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(2); // Ensure it was called again
1080
+ expect(mockTurnRunFn).toHaveBeenLastCalledWith(DEFAULT_GEMINI_FLASH_MODEL, // Still the fallback model
1081
+ [{ text: 'Continue' }], expect.any(Object));
1082
+ });
1083
+ });
1113
1084
  describe('Editor context delta', () => {
1114
1085
  const mockStream = (async function* () {
1115
1086
  yield { type: 'content', value: 'Hello' };
@@ -1126,7 +1097,6 @@ ${JSON.stringify({
1126
1097
  const mockChat = {
1127
1098
  addHistory: vi.fn(),
1128
1099
  setHistory: vi.fn(),
1129
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1130
1100
  // Assume history is not empty for delta checks
1131
1101
  getHistory: vi
1132
1102
  .fn()
@@ -1250,7 +1220,7 @@ ${JSON.stringify({
1250
1220
  },
1251
1221
  };
1252
1222
  // Setup current context
1253
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1223
+ vi.mocked(ideContextStore.get).mockReturnValue({
1254
1224
  workspaceState: {
1255
1225
  openFiles: [
1256
1226
  { ...currentActiveFile, isActive: true, timestamp: Date.now() },
@@ -1296,7 +1266,7 @@ ${JSON.stringify({
1296
1266
  },
1297
1267
  };
1298
1268
  // Setup current context (same as previous)
1299
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1269
+ vi.mocked(ideContextStore.get).mockReturnValue({
1300
1270
  workspaceState: {
1301
1271
  openFiles: [
1302
1272
  { ...activeFile, isActive: true, timestamp: Date.now() },
@@ -1341,11 +1311,10 @@ ${JSON.stringify({
1341
1311
  addHistory: vi.fn(),
1342
1312
  getHistory: vi.fn().mockReturnValue([]), // Default empty history
1343
1313
  setHistory: vi.fn(),
1344
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1345
1314
  };
1346
1315
  client['chat'] = mockChat;
1347
1316
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1348
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1317
+ vi.mocked(ideContextStore.get).mockReturnValue({
1349
1318
  workspaceState: {
1350
1319
  openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
1351
1320
  },
@@ -1421,7 +1390,7 @@ ${JSON.stringify({
1421
1390
  openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
1422
1391
  },
1423
1392
  };
1424
- vi.mocked(ideContext.getIdeContext).mockReturnValue(initialIdeContext);
1393
+ vi.mocked(ideContextStore.get).mockReturnValue(initialIdeContext);
1425
1394
  // Act: Send the tool response
1426
1395
  let stream = client.sendMessageStream([
1427
1396
  {
@@ -1467,7 +1436,7 @@ ${JSON.stringify({
1467
1436
  openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
1468
1437
  },
1469
1438
  };
1470
- vi.mocked(ideContext.getIdeContext).mockReturnValue(newIdeContext);
1439
+ vi.mocked(ideContextStore.get).mockReturnValue(newIdeContext);
1471
1440
  // Act: Send a new, regular user message
1472
1441
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1473
1442
  for await (const _ of stream) {
@@ -1497,7 +1466,7 @@ ${JSON.stringify({
1497
1466
  ],
1498
1467
  },
1499
1468
  };
1500
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextA);
1469
+ vi.mocked(ideContextStore.get).mockReturnValue(contextA);
1501
1470
  // Act: Send a regular message to establish the initial context
1502
1471
  let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
1503
1472
  for await (const _ of stream) {
@@ -1530,7 +1499,7 @@ ${JSON.stringify({
1530
1499
  ],
1531
1500
  },
1532
1501
  };
1533
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextB);
1502
+ vi.mocked(ideContextStore.get).mockReturnValue(contextB);
1534
1503
  // Act: Send the tool response
1535
1504
  stream = client.sendMessageStream([
1536
1505
  {
@@ -1575,7 +1544,7 @@ ${JSON.stringify({
1575
1544
  ],
1576
1545
  },
1577
1546
  };
1578
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextC);
1547
+ vi.mocked(ideContextStore.get).mockReturnValue(contextC);
1579
1548
  // Act: Send a new, regular user message
1580
1549
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1581
1550
  for await (const _ of stream) {
@@ -1640,6 +1609,35 @@ ${JSON.stringify({
1640
1609
  // Assert
1641
1610
  expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
1642
1611
  });
1612
+ it('should abort linked signal when loop is detected', async () => {
1613
+ // Arrange
1614
+ vi.spyOn(client['loopDetector'], 'turnStarted').mockResolvedValue(false);
1615
+ vi.spyOn(client['loopDetector'], 'addAndCheck')
1616
+ .mockReturnValueOnce(false)
1617
+ .mockReturnValueOnce(true);
1618
+ let capturedSignal;
1619
+ mockTurnRunFn.mockImplementation((model, request, signal) => {
1620
+ capturedSignal = signal;
1621
+ return (async function* () {
1622
+ yield { type: 'content', value: 'First event' };
1623
+ yield { type: 'content', value: 'Second event' };
1624
+ })();
1625
+ });
1626
+ const mockChat = {
1627
+ addHistory: vi.fn(),
1628
+ getHistory: vi.fn().mockReturnValue([]),
1629
+ };
1630
+ client['chat'] = mockChat;
1631
+ // Act
1632
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop');
1633
+ const events = [];
1634
+ for await (const event of stream) {
1635
+ events.push(event);
1636
+ }
1637
+ // Assert
1638
+ expect(events).toContainEqual({ type: GeminiEventType.LoopDetected });
1639
+ expect(capturedSignal.aborted).toBe(true);
1640
+ });
1643
1641
  });
1644
1642
  describe('generateContent', () => {
1645
1643
  it('should call generateContent with the correct parameters', async () => {