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

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 (332) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +12 -2
  3. package/dist/index.d.ts +3 -3
  4. package/dist/index.js +3 -3
  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 +32 -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 +5 -4
  49. package/dist/src/core/client.js +80 -140
  50. package/dist/src/core/client.js.map +1 -1
  51. package/dist/src/core/client.test.js +247 -186
  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 +3 -30
  64. package/dist/src/core/geminiChat.js +32 -228
  65. package/dist/src/core/geminiChat.js.map +1 -1
  66. package/dist/src/core/geminiChat.test.js +58 -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 +177 -43
  197. package/dist/src/services/shellExecutionService.js.map +1 -1
  198. package/dist/src/services/shellExecutionService.test.js +153 -56
  199. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  200. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +10 -2
  201. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +85 -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 +12 -2
  206. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +31 -2
  207. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  208. package/dist/src/telemetry/constants.d.ts +3 -0
  209. package/dist/src/telemetry/constants.js +3 -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 +4 -1
  221. package/dist/src/telemetry/loggers.js +42 -7
  222. package/dist/src/telemetry/loggers.js.map +1 -1
  223. package/dist/src/telemetry/loggers.test.js +84 -36
  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 +16 -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 +47 -3
  235. package/dist/src/telemetry/types.js +67 -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/tools.d.ts +7 -5
  269. package/dist/src/tools/tools.js +2 -2
  270. package/dist/src/tools/tools.js.map +1 -1
  271. package/dist/src/tools/write-file.js +4 -5
  272. package/dist/src/tools/write-file.js.map +1 -1
  273. package/dist/src/tools/write-file.test.js +94 -10
  274. package/dist/src/tools/write-file.test.js.map +1 -1
  275. package/dist/src/utils/bfsFileSearch.js +11 -5
  276. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  277. package/dist/src/utils/editCorrector.d.ts +7 -6
  278. package/dist/src/utils/editCorrector.js +61 -18
  279. package/dist/src/utils/editCorrector.js.map +1 -1
  280. package/dist/src/utils/editCorrector.test.js +30 -79
  281. package/dist/src/utils/editCorrector.test.js.map +1 -1
  282. package/dist/src/utils/editor.js +31 -44
  283. package/dist/src/utils/editor.js.map +1 -1
  284. package/dist/src/utils/editor.test.js +61 -75
  285. package/dist/src/utils/editor.test.js.map +1 -1
  286. package/dist/src/utils/errorParsing.js +2 -2
  287. package/dist/src/utils/errorParsing.js.map +1 -1
  288. package/dist/src/utils/errorParsing.test.js +7 -7
  289. package/dist/src/utils/errorParsing.test.js.map +1 -1
  290. package/dist/src/utils/errors.d.ts +6 -0
  291. package/dist/src/utils/errors.js +10 -0
  292. package/dist/src/utils/errors.js.map +1 -1
  293. package/dist/src/utils/fileUtils.test.js +17 -8
  294. package/dist/src/utils/fileUtils.test.js.map +1 -1
  295. package/dist/src/utils/geminiIgnoreParser.d.ts +18 -0
  296. package/dist/src/utils/geminiIgnoreParser.js +61 -0
  297. package/dist/src/utils/geminiIgnoreParser.js.map +1 -0
  298. package/dist/src/utils/geminiIgnoreParser.test.d.ts +6 -0
  299. package/dist/src/utils/geminiIgnoreParser.test.js +50 -0
  300. package/dist/src/utils/geminiIgnoreParser.test.js.map +1 -0
  301. package/dist/src/utils/gitIgnoreParser.d.ts +3 -9
  302. package/dist/src/utils/gitIgnoreParser.js +60 -69
  303. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  304. package/dist/src/utils/gitIgnoreParser.test.js +18 -53
  305. package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
  306. package/dist/src/utils/memoryDiscovery.test.js +12 -6
  307. package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
  308. package/dist/src/utils/nextSpeakerChecker.d.ts +2 -2
  309. package/dist/src/utils/nextSpeakerChecker.js +8 -2
  310. package/dist/src/utils/nextSpeakerChecker.js.map +1 -1
  311. package/dist/src/utils/nextSpeakerChecker.test.js +40 -33
  312. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
  313. package/dist/src/utils/shell-utils.d.ts +5 -0
  314. package/dist/src/utils/shell-utils.js +23 -0
  315. package/dist/src/utils/shell-utils.js.map +1 -1
  316. package/dist/src/utils/terminalSerializer.d.ts +28 -0
  317. package/dist/src/utils/terminalSerializer.js +432 -0
  318. package/dist/src/utils/terminalSerializer.js.map +1 -0
  319. package/dist/src/utils/terminalSerializer.test.d.ts +6 -0
  320. package/dist/src/utils/terminalSerializer.test.js +176 -0
  321. package/dist/src/utils/terminalSerializer.test.js.map +1 -0
  322. package/dist/src/utils/textUtils.d.ts +5 -0
  323. package/dist/src/utils/textUtils.js +14 -0
  324. package/dist/src/utils/textUtils.js.map +1 -1
  325. package/dist/src/utils/textUtils.test.d.ts +6 -0
  326. package/dist/src/utils/textUtils.test.js +59 -0
  327. package/dist/src/utils/textUtils.test.js.map +1 -0
  328. package/dist/tsconfig.tsbuildinfo +1 -1
  329. package/package.json +5 -1
  330. package/dist/src/utils/ide-trust.d.ts +0 -10
  331. package/dist/src/utils/ide-trust.js +0 -14
  332. package/dist/src/utils/ide-trust.js.map +0 -1
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
7
+ import { createUserContent } from '@google/genai';
7
8
  import { findIndexAfterFraction, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
8
9
  import { AuthType, } from './contentGenerator.js';
9
10
  import {} from './geminiChat.js';
@@ -13,7 +14,7 @@ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
13
14
  import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
14
15
  import { setSimulate429 } from '../utils/testUtils.js';
15
16
  import { tokenLimit } from './tokenLimits.js';
16
- import { ideContext } from '../ide/ideContext.js';
17
+ import { ideContextStore } from '../ide/ideContext.js';
17
18
  import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
18
19
  // Mock fs module to prevent actual file system operations during tests
19
20
  const mockFileSystem = new Map();
@@ -108,22 +109,22 @@ describe('findIndexAfterFraction', () => {
108
109
  // 0: 66
109
110
  // 1: 66 + 68 = 134
110
111
  // 2: 134 + 66 = 200
111
- // 200 >= 166.5, so index is 2
112
- expect(findIndexAfterFraction(history, 0.5)).toBe(2);
112
+ // 200 >= 166.5, so index is 3
113
+ expect(findIndexAfterFraction(history, 0.5)).toBe(3);
113
114
  });
114
115
  it('should handle a fraction that results in the last index', () => {
115
116
  // 333 * 0.9 = 299.7
116
117
  // ...
117
118
  // 3: 200 + 68 = 268
118
119
  // 4: 268 + 65 = 333
119
- // 333 >= 299.7, so index is 4
120
- expect(findIndexAfterFraction(history, 0.9)).toBe(4);
120
+ // 333 >= 299.7, so index is 5
121
+ expect(findIndexAfterFraction(history, 0.9)).toBe(5);
121
122
  });
122
123
  it('should handle an empty history', () => {
123
124
  expect(findIndexAfterFraction([], 0.5)).toBe(0);
124
125
  });
125
126
  it('should handle a history with only one item', () => {
126
- expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(0);
127
+ expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(1);
127
128
  });
128
129
  it('should handle history with weird parts', () => {
129
130
  const historyWithEmptyParts = [
@@ -131,7 +132,7 @@ describe('findIndexAfterFraction', () => {
131
132
  { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
132
133
  { role: 'user', parts: [{ text: 'Message 2' }] },
133
134
  ];
134
- expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(1);
135
+ expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(2);
135
136
  });
136
137
  });
137
138
  describe('isThinkingSupported', () => {
@@ -176,8 +177,7 @@ describe('Gemini Client (client.ts)', () => {
176
177
  mockContentGenerator = {
177
178
  generateContent: mockGenerateContentFn,
178
179
  generateContentStream: vi.fn(),
179
- countTokens: vi.fn(),
180
- embedContent: vi.fn(),
180
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
181
181
  batchEmbedContents: vi.fn(),
182
182
  };
183
183
  // Because the GeminiClient constructor kicks off an async process (startChat)
@@ -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,16 +221,26 @@ 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),
228
230
  getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
229
231
  getUseSmartEdit: vi.fn().mockReturnValue(false),
232
+ getUseModelRouter: vi.fn().mockReturnValue(false),
230
233
  getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
231
234
  storage: {
232
235
  getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
233
236
  },
234
237
  getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
238
+ getBaseLlmClient: vi.fn().mockReturnValue({
239
+ generateJson: vi.fn().mockResolvedValue({
240
+ next_speaker: 'user',
241
+ reasoning: 'test',
242
+ }),
243
+ }),
235
244
  };
236
245
  client = new GeminiClient(mockConfig);
237
246
  await client.initialize();
@@ -240,129 +249,6 @@ describe('Gemini Client (client.ts)', () => {
240
249
  afterEach(() => {
241
250
  vi.restoreAllMocks();
242
251
  });
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
252
  describe('addHistory', () => {
367
253
  it('should call chat.addHistory with the provided content', async () => {
368
254
  const mockChat = {
@@ -400,7 +286,6 @@ describe('Gemini Client (client.ts)', () => {
400
286
  });
401
287
  });
402
288
  describe('tryCompressChat', () => {
403
- const mockSendMessage = vi.fn();
404
289
  const mockGetHistory = vi.fn();
405
290
  beforeEach(() => {
406
291
  vi.mock('./tokenLimits', () => ({
@@ -410,7 +295,6 @@ describe('Gemini Client (client.ts)', () => {
410
295
  getHistory: mockGetHistory,
411
296
  addHistory: vi.fn(),
412
297
  setHistory: vi.fn(),
413
- sendMessage: mockSendMessage,
414
298
  };
415
299
  });
416
300
  function setup({ chatHistory = [
@@ -420,7 +304,6 @@ describe('Gemini Client (client.ts)', () => {
420
304
  const mockChat = {
421
305
  getHistory: vi.fn().mockReturnValue(chatHistory),
422
306
  setHistory: vi.fn(),
423
- sendMessage: vi.fn().mockResolvedValue({ text: 'Summary' }),
424
307
  };
425
308
  vi.mocked(mockContentGenerator.countTokens)
426
309
  .mockResolvedValueOnce({ totalTokens: 1000 })
@@ -435,8 +318,12 @@ describe('Gemini Client (client.ts)', () => {
435
318
  vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
436
319
  totalTokens: 1000,
437
320
  });
438
- await client.tryCompressChat('prompt-id-4'); // Fails
439
- const result = await client.tryCompressChat('prompt-id-4', true);
321
+ await client.tryCompressChat('prompt-id-4', false, [
322
+ { text: 'request' },
323
+ ]); // Fails
324
+ const result = await client.tryCompressChat('prompt-id-4', true, [
325
+ { text: 'request' },
326
+ ]);
440
327
  expect(result).toEqual({
441
328
  compressionStatus: CompressionStatus.COMPRESSED,
442
329
  newTokenCount: 1000,
@@ -448,7 +335,9 @@ describe('Gemini Client (client.ts)', () => {
448
335
  vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
449
336
  totalTokens: 1000,
450
337
  });
451
- const result = await client.tryCompressChat('prompt-id-4', true);
338
+ const result = await client.tryCompressChat('prompt-id-4', false, [
339
+ { text: 'request' },
340
+ ]);
452
341
  expect(result).toEqual({
453
342
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
454
343
  newTokenCount: 5000,
@@ -457,7 +346,9 @@ describe('Gemini Client (client.ts)', () => {
457
346
  });
458
347
  it('does not manipulate the source chat', async () => {
459
348
  const { client, mockChat } = setup();
460
- await client.tryCompressChat('prompt-id-4', true);
349
+ await client.tryCompressChat('prompt-id-4', false, [
350
+ { text: 'request' },
351
+ ]);
461
352
  expect(client['chat']).toBe(mockChat); // a new chat session was not created
462
353
  });
463
354
  it('restores the history back to the original', async () => {
@@ -473,14 +364,18 @@ describe('Gemini Client (client.ts)', () => {
473
364
  const { client } = setup({
474
365
  chatHistory: originalHistory,
475
366
  });
476
- const { compressionStatus } = await client.tryCompressChat('prompt-id-4');
367
+ const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false, [{ text: 'what is your wisdom?' }]);
477
368
  expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
478
369
  expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
479
370
  });
480
371
  it('will not attempt to compress context after a failure', async () => {
481
372
  const { client } = setup();
482
- await client.tryCompressChat('prompt-id-4');
483
- const result = await client.tryCompressChat('prompt-id-5');
373
+ await client.tryCompressChat('prompt-id-4', false, [
374
+ { text: 'request' },
375
+ ]);
376
+ const result = await client.tryCompressChat('prompt-id-5', false, [
377
+ { text: 'request' },
378
+ ]);
484
379
  // it counts tokens for {original, compressed} and then never again
485
380
  expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
486
381
  expect(result).toEqual({
@@ -500,7 +395,9 @@ describe('Gemini Client (client.ts)', () => {
500
395
  totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
501
396
  });
502
397
  const initialChat = client.getChat();
503
- const result = await client.tryCompressChat('prompt-id-2');
398
+ const result = await client.tryCompressChat('prompt-id-2', false, [
399
+ { text: '...history...' },
400
+ ]);
504
401
  const newChat = client.getChat();
505
402
  expect(tokenLimit).toHaveBeenCalled();
506
403
  expect(result).toEqual({
@@ -527,11 +424,19 @@ describe('Gemini Client (client.ts)', () => {
527
424
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
528
425
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
529
426
  // Mock the summary response from the chat
530
- mockSendMessage.mockResolvedValue({
531
- role: 'model',
532
- parts: [{ text: 'This is a summary.' }],
427
+ mockGenerateContentFn.mockResolvedValue({
428
+ candidates: [
429
+ {
430
+ content: {
431
+ role: 'model',
432
+ parts: [{ text: 'This is a summary.' }],
433
+ },
434
+ },
435
+ ],
533
436
  });
534
- await client.tryCompressChat('prompt-id-3');
437
+ await client.tryCompressChat('prompt-id-3', false, [
438
+ { text: '...history...' },
439
+ ]);
535
440
  expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
536
441
  tokens_before: originalTokenCount,
537
442
  tokens_after: newTokenCount,
@@ -553,15 +458,23 @@ describe('Gemini Client (client.ts)', () => {
553
458
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
554
459
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
555
460
  // Mock the summary response from the chat
556
- mockSendMessage.mockResolvedValue({
557
- role: 'model',
558
- parts: [{ text: 'This is a summary.' }],
461
+ mockGenerateContentFn.mockResolvedValue({
462
+ candidates: [
463
+ {
464
+ content: {
465
+ role: 'model',
466
+ parts: [{ text: 'This is a summary.' }],
467
+ },
468
+ },
469
+ ],
559
470
  });
560
471
  const initialChat = client.getChat();
561
- const result = await client.tryCompressChat('prompt-id-3');
472
+ const result = await client.tryCompressChat('prompt-id-3', false, [
473
+ { text: '...history...' },
474
+ ]);
562
475
  const newChat = client.getChat();
563
476
  expect(tokenLimit).toHaveBeenCalled();
564
- expect(mockSendMessage).toHaveBeenCalled();
477
+ expect(mockGenerateContentFn).toHaveBeenCalled();
565
478
  // Assert that summarization happened and returned the correct stats
566
479
  expect(result).toEqual({
567
480
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -598,15 +511,23 @@ describe('Gemini Client (client.ts)', () => {
598
511
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
599
512
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
600
513
  // Mock the summary response from the chat
601
- mockSendMessage.mockResolvedValue({
602
- role: 'model',
603
- parts: [{ text: 'This is a summary.' }],
514
+ mockGenerateContentFn.mockResolvedValue({
515
+ candidates: [
516
+ {
517
+ content: {
518
+ role: 'model',
519
+ parts: [{ text: 'This is a summary.' }],
520
+ },
521
+ },
522
+ ],
604
523
  });
605
524
  const initialChat = client.getChat();
606
- const result = await client.tryCompressChat('prompt-id-3');
525
+ const result = await client.tryCompressChat('prompt-id-3', false, [
526
+ { text: '...history...' },
527
+ ]);
607
528
  const newChat = client.getChat();
608
529
  expect(tokenLimit).toHaveBeenCalled();
609
- expect(mockSendMessage).toHaveBeenCalled();
530
+ expect(mockGenerateContentFn).toHaveBeenCalled();
610
531
  // Assert that summarization happened and returned the correct stats
611
532
  expect(result).toEqual({
612
533
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -620,7 +541,7 @@ describe('Gemini Client (client.ts)', () => {
620
541
  // 3. compressed summary message
621
542
  // 4. standard canned user summary message
622
543
  // 5. The last user message (not the last 3 because that would start with a function response)
623
- expect(newChat.getHistory().length).toEqual(5);
544
+ expect(newChat.getHistory().length).toEqual(6);
624
545
  });
625
546
  it('should always trigger summarization when force is true, regardless of token count', async () => {
626
547
  mockGetHistory.mockReturnValue([
@@ -632,14 +553,22 @@ describe('Gemini Client (client.ts)', () => {
632
553
  .mockResolvedValueOnce({ totalTokens: originalTokenCount })
633
554
  .mockResolvedValueOnce({ totalTokens: newTokenCount });
634
555
  // Mock the summary response from the chat
635
- mockSendMessage.mockResolvedValue({
636
- role: 'model',
637
- parts: [{ text: 'This is a summary.' }],
556
+ mockGenerateContentFn.mockResolvedValue({
557
+ candidates: [
558
+ {
559
+ content: {
560
+ role: 'model',
561
+ parts: [{ text: 'This is a summary.' }],
562
+ },
563
+ },
564
+ ],
638
565
  });
639
566
  const initialChat = client.getChat();
640
- const result = await client.tryCompressChat('prompt-id-1', true); // force = true
567
+ const result = await client.tryCompressChat('prompt-id-1', false, [
568
+ { text: '...history...' },
569
+ ]); // force = true
641
570
  const newChat = client.getChat();
642
- expect(mockSendMessage).toHaveBeenCalled();
571
+ expect(mockGenerateContentFn).toHaveBeenCalled();
643
572
  expect(result).toEqual({
644
573
  compressionStatus: CompressionStatus.COMPRESSED,
645
574
  originalTokenCount,
@@ -665,17 +594,18 @@ describe('Gemini Client (client.ts)', () => {
665
594
  { role: 'model', parts: [{ text: 'Long response' }] },
666
595
  ];
667
596
  const mockChat = {
668
- getHistory: vi.fn().mockReturnValue(mockChatHistory),
597
+ getHistory: vi.fn().mockImplementation(() => [...mockChatHistory]),
669
598
  setHistory: vi.fn(),
670
599
  sendMessage: mockSendMessage,
671
600
  };
672
601
  client['chat'] = mockChat;
673
602
  client['startChat'] = vi.fn().mockResolvedValue(mockChat);
674
- const result = await client.tryCompressChat('prompt-id-4', true);
603
+ const request = [{ text: 'Long conversation' }];
604
+ const result = await client.tryCompressChat('prompt-id-4', false, request);
675
605
  expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
676
606
  expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(1, {
677
607
  model: firstCurrentModel,
678
- contents: mockChatHistory,
608
+ contents: [...mockChatHistory, createUserContent(request)],
679
609
  });
680
610
  expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(2, {
681
611
  model: secondCurrentModel,
@@ -740,7 +670,7 @@ describe('Gemini Client (client.ts)', () => {
740
670
  });
741
671
  it('should include editor context when ideMode is enabled', async () => {
742
672
  // Arrange
743
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
673
+ vi.mocked(ideContextStore.get).mockReturnValue({
744
674
  workspaceState: {
745
675
  openFiles: [
746
676
  {
@@ -762,6 +692,11 @@ describe('Gemini Client (client.ts)', () => {
762
692
  },
763
693
  });
764
694
  vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
695
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
696
+ originalTokenCount: 0,
697
+ newTokenCount: 0,
698
+ compressionStatus: CompressionStatus.COMPRESSED,
699
+ });
765
700
  mockTurnRunFn.mockReturnValue((async function* () {
766
701
  yield { type: 'content', value: 'Hello' };
767
702
  })());
@@ -777,7 +712,7 @@ describe('Gemini Client (client.ts)', () => {
777
712
  // consume stream
778
713
  }
779
714
  // Assert
780
- expect(ideContext.getIdeContext).toHaveBeenCalled();
715
+ expect(ideContextStore.get).toHaveBeenCalled();
781
716
  const expectedContext = `
782
717
  Here is the user's editor context as a JSON object. This is for your information only.
783
718
  \`\`\`json
@@ -802,7 +737,7 @@ ${JSON.stringify({
802
737
  });
803
738
  it('should not add context if ideMode is enabled but no open files', async () => {
804
739
  // Arrange
805
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
740
+ vi.mocked(ideContextStore.get).mockReturnValue({
806
741
  workspaceState: {
807
742
  openFiles: [],
808
743
  },
@@ -824,12 +759,16 @@ ${JSON.stringify({
824
759
  // consume stream
825
760
  }
826
761
  // Assert
827
- expect(ideContext.getIdeContext).toHaveBeenCalled();
828
- expect(mockTurnRunFn).toHaveBeenCalledWith(initialRequest, expect.any(Object));
762
+ expect(ideContextStore.get).toHaveBeenCalled();
763
+ // The `turn.run` method is now called with the model name as the first
764
+ // argument. We use `expect.any(String)` because this test is
765
+ // concerned with the IDE context logic, not the model routing,
766
+ // which is tested in its own dedicated suite.
767
+ expect(mockTurnRunFn).toHaveBeenCalledWith(expect.any(String), initialRequest, expect.any(Object));
829
768
  });
830
769
  it('should add context if ideMode is enabled and there is one active file', async () => {
831
770
  // Arrange
832
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
771
+ vi.mocked(ideContextStore.get).mockReturnValue({
833
772
  workspaceState: {
834
773
  openFiles: [
835
774
  {
@@ -843,6 +782,11 @@ ${JSON.stringify({
843
782
  },
844
783
  });
845
784
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
785
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
786
+ originalTokenCount: 0,
787
+ newTokenCount: 0,
788
+ compressionStatus: CompressionStatus.COMPRESSED,
789
+ });
846
790
  const mockStream = (async function* () {
847
791
  yield { type: 'content', value: 'Hello' };
848
792
  })();
@@ -859,7 +803,7 @@ ${JSON.stringify({
859
803
  // consume stream
860
804
  }
861
805
  // Assert
862
- expect(ideContext.getIdeContext).toHaveBeenCalled();
806
+ expect(ideContextStore.get).toHaveBeenCalled();
863
807
  const expectedContext = `
864
808
  Here is the user's editor context as a JSON object. This is for your information only.
865
809
  \`\`\`json
@@ -883,7 +827,7 @@ ${JSON.stringify({
883
827
  });
884
828
  it('should add context if ideMode is enabled and there are open files but no active file', async () => {
885
829
  // Arrange
886
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
830
+ vi.mocked(ideContextStore.get).mockReturnValue({
887
831
  workspaceState: {
888
832
  openFiles: [
889
833
  {
@@ -898,6 +842,11 @@ ${JSON.stringify({
898
842
  },
899
843
  });
900
844
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
845
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
846
+ originalTokenCount: 0,
847
+ newTokenCount: 0,
848
+ compressionStatus: CompressionStatus.COMPRESSED,
849
+ });
901
850
  const mockStream = (async function* () {
902
851
  yield { type: 'content', value: 'Hello' };
903
852
  })();
@@ -914,7 +863,7 @@ ${JSON.stringify({
914
863
  // consume stream
915
864
  }
916
865
  // Assert
917
- expect(ideContext.getIdeContext).toHaveBeenCalled();
866
+ expect(ideContextStore.get).toHaveBeenCalled();
918
867
  const expectedContext = `
919
868
  Here is the user's editor context as a JSON object. This is for your information only.
920
869
  \`\`\`json
@@ -1110,6 +1059,91 @@ ${JSON.stringify({
1110
1059
  console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
1111
1060
  `${eventCount} events generated (properly bounded by MAX_TURNS)`);
1112
1061
  });
1062
+ describe('Model Routing', () => {
1063
+ let mockRouterService;
1064
+ beforeEach(() => {
1065
+ mockRouterService = {
1066
+ route: vi
1067
+ .fn()
1068
+ .mockResolvedValue({ model: 'routed-model', reason: 'test' }),
1069
+ };
1070
+ vi.mocked(mockConfig.getModelRouterService).mockReturnValue(mockRouterService);
1071
+ mockTurnRunFn.mockReturnValue((async function* () {
1072
+ yield { type: 'content', value: 'Hello' };
1073
+ })());
1074
+ });
1075
+ it('should use the model router service to select a model on the first turn', async () => {
1076
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1077
+ await fromAsync(stream); // consume stream
1078
+ expect(mockConfig.getModelRouterService).toHaveBeenCalled();
1079
+ expect(mockRouterService.route).toHaveBeenCalled();
1080
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', // The model from the router
1081
+ [{ text: 'Hi' }], expect.any(Object));
1082
+ });
1083
+ it('should use the same model for subsequent turns in the same prompt (stickiness)', async () => {
1084
+ // First turn
1085
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1086
+ await fromAsync(stream);
1087
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1088
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1089
+ // Second turn
1090
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-1');
1091
+ await fromAsync(stream);
1092
+ // Router should not be called again
1093
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1094
+ // Should stick to the first model
1095
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Continue' }], expect.any(Object));
1096
+ });
1097
+ it('should reset the sticky model and re-route when the prompt_id changes', async () => {
1098
+ // First prompt
1099
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1100
+ await fromAsync(stream);
1101
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1102
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1103
+ // New prompt
1104
+ mockRouterService.route.mockResolvedValue({
1105
+ model: 'new-routed-model',
1106
+ reason: 'test',
1107
+ });
1108
+ stream = client.sendMessageStream([{ text: 'A new topic' }], new AbortController().signal, 'prompt-2');
1109
+ await fromAsync(stream);
1110
+ // Router should be called again for the new prompt
1111
+ expect(mockRouterService.route).toHaveBeenCalledTimes(2);
1112
+ // Should use the newly routed model
1113
+ expect(mockTurnRunFn).toHaveBeenCalledWith('new-routed-model', [{ text: 'A new topic' }], expect.any(Object));
1114
+ });
1115
+ it('should use the fallback model and bypass routing when in fallback mode', async () => {
1116
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1117
+ mockRouterService.route.mockResolvedValue({
1118
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1119
+ reason: 'fallback',
1120
+ });
1121
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1122
+ await fromAsync(stream);
1123
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1124
+ });
1125
+ it('should stick to the fallback model for the entire sequence even if fallback mode ends', async () => {
1126
+ // Start the sequence in fallback mode
1127
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1128
+ mockRouterService.route.mockResolvedValue({
1129
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1130
+ reason: 'fallback',
1131
+ });
1132
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-fallback-stickiness');
1133
+ await fromAsync(stream);
1134
+ // First call should use fallback model
1135
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1136
+ // End fallback mode
1137
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(false);
1138
+ // Second call in the same sequence
1139
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-fallback-stickiness');
1140
+ await fromAsync(stream);
1141
+ // Router should still not be called, and it should stick to the fallback model
1142
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(2); // Ensure it was called again
1143
+ expect(mockTurnRunFn).toHaveBeenLastCalledWith(DEFAULT_GEMINI_FLASH_MODEL, // Still the fallback model
1144
+ [{ text: 'Continue' }], expect.any(Object));
1145
+ });
1146
+ });
1113
1147
  describe('Editor context delta', () => {
1114
1148
  const mockStream = (async function* () {
1115
1149
  yield { type: 'content', value: 'Hello' };
@@ -1126,7 +1160,6 @@ ${JSON.stringify({
1126
1160
  const mockChat = {
1127
1161
  addHistory: vi.fn(),
1128
1162
  setHistory: vi.fn(),
1129
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1130
1163
  // Assume history is not empty for delta checks
1131
1164
  getHistory: vi
1132
1165
  .fn()
@@ -1250,7 +1283,7 @@ ${JSON.stringify({
1250
1283
  },
1251
1284
  };
1252
1285
  // Setup current context
1253
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1286
+ vi.mocked(ideContextStore.get).mockReturnValue({
1254
1287
  workspaceState: {
1255
1288
  openFiles: [
1256
1289
  { ...currentActiveFile, isActive: true, timestamp: Date.now() },
@@ -1296,7 +1329,7 @@ ${JSON.stringify({
1296
1329
  },
1297
1330
  };
1298
1331
  // Setup current context (same as previous)
1299
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1332
+ vi.mocked(ideContextStore.get).mockReturnValue({
1300
1333
  workspaceState: {
1301
1334
  openFiles: [
1302
1335
  { ...activeFile, isActive: true, timestamp: Date.now() },
@@ -1341,11 +1374,10 @@ ${JSON.stringify({
1341
1374
  addHistory: vi.fn(),
1342
1375
  getHistory: vi.fn().mockReturnValue([]), // Default empty history
1343
1376
  setHistory: vi.fn(),
1344
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1345
1377
  };
1346
1378
  client['chat'] = mockChat;
1347
1379
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1348
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1380
+ vi.mocked(ideContextStore.get).mockReturnValue({
1349
1381
  workspaceState: {
1350
1382
  openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
1351
1383
  },
@@ -1421,7 +1453,7 @@ ${JSON.stringify({
1421
1453
  openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
1422
1454
  },
1423
1455
  };
1424
- vi.mocked(ideContext.getIdeContext).mockReturnValue(initialIdeContext);
1456
+ vi.mocked(ideContextStore.get).mockReturnValue(initialIdeContext);
1425
1457
  // Act: Send the tool response
1426
1458
  let stream = client.sendMessageStream([
1427
1459
  {
@@ -1467,7 +1499,7 @@ ${JSON.stringify({
1467
1499
  openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
1468
1500
  },
1469
1501
  };
1470
- vi.mocked(ideContext.getIdeContext).mockReturnValue(newIdeContext);
1502
+ vi.mocked(ideContextStore.get).mockReturnValue(newIdeContext);
1471
1503
  // Act: Send a new, regular user message
1472
1504
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1473
1505
  for await (const _ of stream) {
@@ -1497,7 +1529,7 @@ ${JSON.stringify({
1497
1529
  ],
1498
1530
  },
1499
1531
  };
1500
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextA);
1532
+ vi.mocked(ideContextStore.get).mockReturnValue(contextA);
1501
1533
  // Act: Send a regular message to establish the initial context
1502
1534
  let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
1503
1535
  for await (const _ of stream) {
@@ -1530,7 +1562,7 @@ ${JSON.stringify({
1530
1562
  ],
1531
1563
  },
1532
1564
  };
1533
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextB);
1565
+ vi.mocked(ideContextStore.get).mockReturnValue(contextB);
1534
1566
  // Act: Send the tool response
1535
1567
  stream = client.sendMessageStream([
1536
1568
  {
@@ -1575,7 +1607,7 @@ ${JSON.stringify({
1575
1607
  ],
1576
1608
  },
1577
1609
  };
1578
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextC);
1610
+ vi.mocked(ideContextStore.get).mockReturnValue(contextC);
1579
1611
  // Act: Send a new, regular user message
1580
1612
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1581
1613
  for await (const _ of stream) {
@@ -1640,6 +1672,35 @@ ${JSON.stringify({
1640
1672
  // Assert
1641
1673
  expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
1642
1674
  });
1675
+ it('should abort linked signal when loop is detected', async () => {
1676
+ // Arrange
1677
+ vi.spyOn(client['loopDetector'], 'turnStarted').mockResolvedValue(false);
1678
+ vi.spyOn(client['loopDetector'], 'addAndCheck')
1679
+ .mockReturnValueOnce(false)
1680
+ .mockReturnValueOnce(true);
1681
+ let capturedSignal;
1682
+ mockTurnRunFn.mockImplementation((model, request, signal) => {
1683
+ capturedSignal = signal;
1684
+ return (async function* () {
1685
+ yield { type: 'content', value: 'First event' };
1686
+ yield { type: 'content', value: 'Second event' };
1687
+ })();
1688
+ });
1689
+ const mockChat = {
1690
+ addHistory: vi.fn(),
1691
+ getHistory: vi.fn().mockReturnValue([]),
1692
+ };
1693
+ client['chat'] = mockChat;
1694
+ // Act
1695
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop');
1696
+ const events = [];
1697
+ for await (const event of stream) {
1698
+ events.push(event);
1699
+ }
1700
+ // Assert
1701
+ expect(events).toContainEqual({ type: GeminiEventType.LoopDetected });
1702
+ expect(capturedSignal.aborted).toBe(true);
1703
+ });
1643
1704
  });
1644
1705
  describe('generateContent', () => {
1645
1706
  it('should call generateContent with the correct parameters', async () => {