@google/gemini-cli-core 0.0.3-preview.4 → 0.0.3

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