@machina.ai/cell-cli-core 1.4.0-rc2 → 1.6.1-rc1

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 (421) hide show
  1. package/dist/index.d.ts +6 -4
  2. package/dist/index.js +6 -4
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +6 -2
  5. package/dist/src/code_assist/codeAssist.js +1 -1
  6. package/dist/src/code_assist/codeAssist.js.map +1 -1
  7. package/dist/src/code_assist/converter.d.ts +1 -0
  8. package/dist/src/code_assist/converter.js +1 -0
  9. package/dist/src/code_assist/converter.js.map +1 -1
  10. package/dist/src/code_assist/converter.test.js +10 -0
  11. package/dist/src/code_assist/converter.test.js.map +1 -1
  12. package/dist/src/code_assist/oauth-credential-storage.d.ts +25 -0
  13. package/dist/src/code_assist/oauth-credential-storage.js +109 -0
  14. package/dist/src/code_assist/oauth-credential-storage.js.map +1 -0
  15. package/dist/src/code_assist/oauth-credential-storage.test.js +136 -0
  16. package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -0
  17. package/dist/src/code_assist/oauth2.js +28 -2
  18. package/dist/src/code_assist/oauth2.js.map +1 -1
  19. package/dist/src/code_assist/oauth2.test.js +674 -536
  20. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  21. package/dist/src/code_assist/server.d.ts +1 -1
  22. package/dist/src/code_assist/server.js +24 -1
  23. package/dist/src/code_assist/server.js.map +1 -1
  24. package/dist/src/code_assist/server.test.js +25 -0
  25. package/dist/src/code_assist/server.test.js.map +1 -1
  26. package/dist/src/code_assist/types.d.ts +17 -2
  27. package/dist/src/config/config.d.ts +62 -5
  28. package/dist/src/config/config.js +154 -42
  29. package/dist/src/config/config.js.map +1 -1
  30. package/dist/src/config/config.test.js +235 -137
  31. package/dist/src/config/config.test.js.map +1 -1
  32. package/dist/src/config/models.d.ts +15 -0
  33. package/dist/src/config/models.js +27 -0
  34. package/dist/src/config/models.js.map +1 -1
  35. package/dist/src/config/models.test.d.ts +6 -0
  36. package/dist/src/config/models.test.js +55 -0
  37. package/dist/src/config/models.test.js.map +1 -0
  38. package/dist/src/config/storage.d.ts +2 -0
  39. package/dist/src/config/storage.js +6 -1
  40. package/dist/src/config/storage.js.map +1 -1
  41. package/dist/src/config/storage.test.js +4 -0
  42. package/dist/src/config/storage.test.js.map +1 -1
  43. package/dist/src/confirmation-bus/index.d.ts +7 -0
  44. package/dist/src/confirmation-bus/index.js +8 -0
  45. package/dist/src/confirmation-bus/index.js.map +1 -0
  46. package/dist/src/confirmation-bus/message-bus.d.ts +17 -0
  47. package/dist/src/confirmation-bus/message-bus.js +81 -0
  48. package/dist/src/confirmation-bus/message-bus.js.map +1 -0
  49. package/dist/src/confirmation-bus/message-bus.test.d.ts +6 -0
  50. package/dist/src/confirmation-bus/message-bus.test.js +164 -0
  51. package/dist/src/confirmation-bus/message-bus.test.js.map +1 -0
  52. package/dist/src/confirmation-bus/types.d.ts +38 -0
  53. package/dist/src/confirmation-bus/types.js +15 -0
  54. package/dist/src/confirmation-bus/types.js.map +1 -0
  55. package/dist/src/core/baseLlmClient.d.ts +46 -0
  56. package/dist/src/core/baseLlmClient.js +112 -0
  57. package/dist/src/core/baseLlmClient.js.map +1 -0
  58. package/dist/src/core/baseLlmClient.test.d.ts +6 -0
  59. package/dist/src/core/baseLlmClient.test.js +253 -0
  60. package/dist/src/core/baseLlmClient.test.js.map +1 -0
  61. package/dist/src/core/client.d.ts +8 -18
  62. package/dist/src/core/client.js +108 -227
  63. package/dist/src/core/client.js.map +1 -1
  64. package/dist/src/core/client.test.js +269 -491
  65. package/dist/src/core/client.test.js.map +1 -1
  66. package/dist/src/core/contentGenerator.d.ts +0 -1
  67. package/dist/src/core/contentGenerator.js +0 -4
  68. package/dist/src/core/contentGenerator.js.map +1 -1
  69. package/dist/src/core/contentGenerator.test.js +1 -3
  70. package/dist/src/core/contentGenerator.test.js.map +1 -1
  71. package/dist/src/core/coreToolScheduler.d.ts +8 -3
  72. package/dist/src/core/coreToolScheduler.js +118 -8
  73. package/dist/src/core/coreToolScheduler.js.map +1 -1
  74. package/dist/src/core/coreToolScheduler.test.js +314 -5
  75. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  76. package/dist/src/core/geminiChat.d.ts +15 -38
  77. package/dist/src/core/geminiChat.js +108 -257
  78. package/dist/src/core/geminiChat.js.map +1 -1
  79. package/dist/src/core/geminiChat.test.js +429 -491
  80. package/dist/src/core/geminiChat.test.js.map +1 -1
  81. package/dist/src/core/loggingContentGenerator.js +7 -10
  82. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  83. package/dist/src/core/nonInteractiveToolExecutor.test.js +57 -1
  84. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  85. package/dist/src/core/prompts.d.ts +5 -0
  86. package/dist/src/core/prompts.js +64 -43
  87. package/dist/src/core/prompts.js.map +1 -1
  88. package/dist/src/core/prompts.test.js +146 -17
  89. package/dist/src/core/prompts.test.js.map +1 -1
  90. package/dist/src/core/subagent.js +2 -4
  91. package/dist/src/core/subagent.js.map +1 -1
  92. package/dist/src/core/subagent.test.js +12 -13
  93. package/dist/src/core/subagent.test.js.map +1 -1
  94. package/dist/src/core/turn.d.ts +3 -1
  95. package/dist/src/core/turn.js +2 -2
  96. package/dist/src/core/turn.js.map +1 -1
  97. package/dist/src/core/turn.test.js +18 -18
  98. package/dist/src/core/turn.test.js.map +1 -1
  99. package/dist/src/fallback/handler.d.ts +7 -0
  100. package/dist/src/fallback/handler.js +51 -0
  101. package/dist/src/fallback/handler.js.map +1 -0
  102. package/dist/src/fallback/handler.test.d.ts +6 -0
  103. package/dist/src/fallback/handler.test.js +130 -0
  104. package/dist/src/fallback/handler.test.js.map +1 -0
  105. package/dist/src/fallback/types.d.ts +14 -0
  106. package/dist/src/fallback/types.js +7 -0
  107. package/dist/src/fallback/types.js.map +1 -0
  108. package/dist/src/generated/git-commit.d.ts +2 -2
  109. package/dist/src/generated/git-commit.js +2 -2
  110. package/dist/src/generated/git-commit.js.map +1 -1
  111. package/dist/src/ide/constants.d.ts +3 -0
  112. package/dist/src/ide/constants.js +3 -0
  113. package/dist/src/ide/constants.js.map +1 -1
  114. package/dist/src/ide/ide-client.d.ts +51 -13
  115. package/dist/src/ide/ide-client.js +241 -35
  116. package/dist/src/ide/ide-client.js.map +1 -1
  117. package/dist/src/ide/ide-client.test.js +236 -0
  118. package/dist/src/ide/ide-client.test.js.map +1 -1
  119. package/dist/src/ide/ide-installer.js +8 -2
  120. package/dist/src/ide/ide-installer.js.map +1 -1
  121. package/dist/src/ide/ide-installer.test.js +13 -2
  122. package/dist/src/ide/ide-installer.test.js.map +1 -1
  123. package/dist/src/ide/ideContext.d.ts +35 -377
  124. package/dist/src/ide/ideContext.js +60 -107
  125. package/dist/src/ide/ideContext.js.map +1 -1
  126. package/dist/src/ide/ideContext.test.js +152 -24
  127. package/dist/src/ide/ideContext.test.js.map +1 -1
  128. package/dist/src/ide/process-utils.js +8 -1
  129. package/dist/src/ide/process-utils.js.map +1 -1
  130. package/dist/src/ide/types.d.ts +486 -0
  131. package/dist/src/ide/types.js +138 -0
  132. package/dist/src/ide/types.js.map +1 -0
  133. package/dist/src/index.d.ts +6 -1
  134. package/dist/src/index.js +6 -1
  135. package/dist/src/index.js.map +1 -1
  136. package/dist/src/mcp/oauth-provider.d.ts +1 -0
  137. package/dist/src/mcp/oauth-provider.js +22 -17
  138. package/dist/src/mcp/oauth-provider.js.map +1 -1
  139. package/dist/src/mcp/oauth-provider.test.js +149 -13
  140. package/dist/src/mcp/oauth-provider.test.js.map +1 -1
  141. package/dist/src/mcp/oauth-token-storage.d.ts +10 -6
  142. package/dist/src/mcp/oauth-token-storage.js +48 -16
  143. package/dist/src/mcp/oauth-token-storage.js.map +1 -1
  144. package/dist/src/mcp/oauth-token-storage.test.js +254 -163
  145. package/dist/src/mcp/oauth-token-storage.test.js.map +1 -1
  146. package/dist/src/mcp/oauth-utils.js +1 -0
  147. package/dist/src/mcp/oauth-utils.js.map +1 -1
  148. package/dist/src/mcp/token-storage/index.d.ts +11 -0
  149. package/dist/src/mcp/token-storage/index.js +12 -0
  150. package/dist/src/mcp/token-storage/index.js.map +1 -0
  151. package/dist/src/output/json-formatter.d.ts +11 -0
  152. package/dist/src/output/json-formatter.js +30 -0
  153. package/dist/src/output/json-formatter.js.map +1 -0
  154. package/dist/src/output/json-formatter.test.d.ts +6 -0
  155. package/dist/src/output/json-formatter.test.js +266 -0
  156. package/dist/src/output/json-formatter.test.js.map +1 -0
  157. package/dist/src/output/types.d.ts +20 -0
  158. package/dist/src/output/types.js +11 -0
  159. package/dist/src/output/types.js.map +1 -0
  160. package/dist/src/policy/index.d.ts +7 -0
  161. package/dist/src/policy/index.js +8 -0
  162. package/dist/src/policy/index.js.map +1 -0
  163. package/dist/src/policy/policy-engine.d.ts +30 -0
  164. package/dist/src/policy/policy-engine.js +83 -0
  165. package/dist/src/policy/policy-engine.js.map +1 -0
  166. package/dist/src/policy/policy-engine.test.d.ts +6 -0
  167. package/dist/src/policy/policy-engine.test.js +470 -0
  168. package/dist/src/policy/policy-engine.test.js.map +1 -0
  169. package/dist/src/policy/stable-stringify.d.ts +58 -0
  170. package/dist/src/policy/stable-stringify.js +122 -0
  171. package/dist/src/policy/stable-stringify.js.map +1 -0
  172. package/dist/src/policy/types.d.ts +47 -0
  173. package/dist/src/policy/types.js +12 -0
  174. package/dist/src/policy/types.js.map +1 -0
  175. package/dist/src/routing/modelRouterService.d.ts +23 -0
  176. package/dist/src/routing/modelRouterService.js +70 -0
  177. package/dist/src/routing/modelRouterService.js.map +1 -0
  178. package/dist/src/routing/modelRouterService.test.d.ts +6 -0
  179. package/dist/src/routing/modelRouterService.test.js +98 -0
  180. package/dist/src/routing/modelRouterService.test.js.map +1 -0
  181. package/dist/src/routing/routingStrategy.d.ts +62 -0
  182. package/dist/src/routing/routingStrategy.js +7 -0
  183. package/dist/src/routing/routingStrategy.js.map +1 -0
  184. package/dist/src/routing/strategies/classifierStrategy.d.ts +12 -0
  185. package/dist/src/routing/strategies/classifierStrategy.js +173 -0
  186. package/dist/src/routing/strategies/classifierStrategy.js.map +1 -0
  187. package/dist/src/routing/strategies/classifierStrategy.test.d.ts +6 -0
  188. package/dist/src/routing/strategies/classifierStrategy.test.js +192 -0
  189. package/dist/src/routing/strategies/classifierStrategy.test.js.map +1 -0
  190. package/dist/src/routing/strategies/compositeStrategy.d.ts +26 -0
  191. package/dist/src/routing/strategies/compositeStrategy.js +67 -0
  192. package/dist/src/routing/strategies/compositeStrategy.js.map +1 -0
  193. package/dist/src/routing/strategies/compositeStrategy.test.d.ts +6 -0
  194. package/dist/src/routing/strategies/compositeStrategy.test.js +123 -0
  195. package/dist/src/routing/strategies/compositeStrategy.test.js.map +1 -0
  196. package/dist/src/routing/strategies/defaultStrategy.d.ts +12 -0
  197. package/dist/src/routing/strategies/defaultStrategy.js +20 -0
  198. package/dist/src/routing/strategies/defaultStrategy.js.map +1 -0
  199. package/dist/src/routing/strategies/defaultStrategy.test.d.ts +6 -0
  200. package/dist/src/routing/strategies/defaultStrategy.test.js +26 -0
  201. package/dist/src/routing/strategies/defaultStrategy.test.js.map +1 -0
  202. package/dist/src/routing/strategies/fallbackStrategy.d.ts +12 -0
  203. package/dist/src/routing/strategies/fallbackStrategy.js +25 -0
  204. package/dist/src/routing/strategies/fallbackStrategy.js.map +1 -0
  205. package/dist/src/routing/strategies/fallbackStrategy.test.d.ts +6 -0
  206. package/dist/src/routing/strategies/fallbackStrategy.test.js +55 -0
  207. package/dist/src/routing/strategies/fallbackStrategy.test.js.map +1 -0
  208. package/dist/src/routing/strategies/overrideStrategy.d.ts +15 -0
  209. package/dist/src/routing/strategies/overrideStrategy.js +28 -0
  210. package/dist/src/routing/strategies/overrideStrategy.js.map +1 -0
  211. package/dist/src/routing/strategies/overrideStrategy.test.d.ts +6 -0
  212. package/dist/src/routing/strategies/overrideStrategy.test.js +42 -0
  213. package/dist/src/routing/strategies/overrideStrategy.test.js.map +1 -0
  214. package/dist/src/services/chatRecordingService.d.ts +2 -1
  215. package/dist/src/services/chatRecordingService.js +3 -3
  216. package/dist/src/services/chatRecordingService.js.map +1 -1
  217. package/dist/src/services/chatRecordingService.test.js +8 -3
  218. package/dist/src/services/chatRecordingService.test.js.map +1 -1
  219. package/dist/src/services/fileDiscoveryService.d.ts +10 -0
  220. package/dist/src/services/fileDiscoveryService.js +31 -17
  221. package/dist/src/services/fileDiscoveryService.js.map +1 -1
  222. package/dist/src/services/gitService.js +9 -12
  223. package/dist/src/services/gitService.js.map +1 -1
  224. package/dist/src/services/gitService.test.js +10 -20
  225. package/dist/src/services/gitService.test.js.map +1 -1
  226. package/dist/src/services/loopDetectionService.d.ts +5 -0
  227. package/dist/src/services/loopDetectionService.js +36 -20
  228. package/dist/src/services/loopDetectionService.js.map +1 -1
  229. package/dist/src/services/loopDetectionService.test.js +41 -12
  230. package/dist/src/services/loopDetectionService.test.js.map +1 -1
  231. package/dist/src/services/shellExecutionService.d.ts +34 -2
  232. package/dist/src/services/shellExecutionService.js +192 -43
  233. package/dist/src/services/shellExecutionService.js.map +1 -1
  234. package/dist/src/services/shellExecutionService.test.js +184 -55
  235. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  236. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +14 -2
  237. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +107 -5
  238. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  239. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +82 -5
  240. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  241. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +13 -2
  242. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +33 -2
  243. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  244. package/dist/src/telemetry/constants.d.ts +7 -0
  245. package/dist/src/telemetry/constants.js +7 -0
  246. package/dist/src/telemetry/constants.js.map +1 -1
  247. package/dist/src/telemetry/gcp-exporters.d.ts +34 -0
  248. package/dist/src/telemetry/gcp-exporters.js +117 -0
  249. package/dist/src/telemetry/gcp-exporters.js.map +1 -0
  250. package/dist/src/telemetry/gcp-exporters.test.d.ts +6 -0
  251. package/dist/src/telemetry/gcp-exporters.test.js +318 -0
  252. package/dist/src/telemetry/gcp-exporters.test.js.map +1 -0
  253. package/dist/src/telemetry/high-water-mark-tracker.d.ts +43 -0
  254. package/dist/src/telemetry/high-water-mark-tracker.js +88 -0
  255. package/dist/src/telemetry/high-water-mark-tracker.js.map +1 -0
  256. package/dist/src/telemetry/high-water-mark-tracker.test.d.ts +6 -0
  257. package/dist/src/telemetry/high-water-mark-tracker.test.js +152 -0
  258. package/dist/src/telemetry/high-water-mark-tracker.test.js.map +1 -0
  259. package/dist/src/telemetry/index.d.ts +5 -2
  260. package/dist/src/telemetry/index.js +5 -2
  261. package/dist/src/telemetry/index.js.map +1 -1
  262. package/dist/src/telemetry/loggers.d.ts +8 -1
  263. package/dist/src/telemetry/loggers.js +114 -7
  264. package/dist/src/telemetry/loggers.js.map +1 -1
  265. package/dist/src/telemetry/loggers.test.js +232 -39
  266. package/dist/src/telemetry/loggers.test.js.map +1 -1
  267. package/dist/src/telemetry/metrics.d.ts +3 -1
  268. package/dist/src/telemetry/metrics.js +32 -3
  269. package/dist/src/telemetry/metrics.js.map +1 -1
  270. package/dist/src/telemetry/metrics.test.js +42 -0
  271. package/dist/src/telemetry/metrics.test.js.map +1 -1
  272. package/dist/src/telemetry/rate-limiter.d.ts +48 -0
  273. package/dist/src/telemetry/rate-limiter.js +100 -0
  274. package/dist/src/telemetry/rate-limiter.js.map +1 -0
  275. package/dist/src/telemetry/rate-limiter.test.d.ts +6 -0
  276. package/dist/src/telemetry/rate-limiter.test.js +207 -0
  277. package/dist/src/telemetry/rate-limiter.test.js.map +1 -0
  278. package/dist/src/telemetry/sdk.js +19 -1
  279. package/dist/src/telemetry/sdk.js.map +1 -1
  280. package/dist/src/telemetry/sdk.test.js +95 -0
  281. package/dist/src/telemetry/sdk.test.js.map +1 -1
  282. package/dist/src/telemetry/types.d.ts +60 -3
  283. package/dist/src/telemetry/types.js +93 -3
  284. package/dist/src/telemetry/types.js.map +1 -1
  285. package/dist/src/tools/edit.js +12 -5
  286. package/dist/src/tools/edit.js.map +1 -1
  287. package/dist/src/tools/edit.test.js +120 -9
  288. package/dist/src/tools/edit.test.js.map +1 -1
  289. package/dist/src/tools/glob.d.ts +5 -1
  290. package/dist/src/tools/glob.js +24 -17
  291. package/dist/src/tools/glob.js.map +1 -1
  292. package/dist/src/tools/glob.test.js +51 -0
  293. package/dist/src/tools/glob.test.js.map +1 -1
  294. package/dist/src/tools/ls.js +19 -32
  295. package/dist/src/tools/ls.js.map +1 -1
  296. package/dist/src/tools/ls.test.js +140 -280
  297. package/dist/src/tools/ls.test.js.map +1 -1
  298. package/dist/src/tools/mcp-client-manager.js +5 -21
  299. package/dist/src/tools/mcp-client-manager.js.map +1 -1
  300. package/dist/src/tools/mcp-client.js +5 -5
  301. package/dist/src/tools/mcp-client.js.map +1 -1
  302. package/dist/src/tools/mcp-tool.js +30 -2
  303. package/dist/src/tools/mcp-tool.js.map +1 -1
  304. package/dist/src/tools/mcp-tool.test.js +117 -0
  305. package/dist/src/tools/mcp-tool.test.js.map +1 -1
  306. package/dist/src/tools/read-file.js +7 -2
  307. package/dist/src/tools/read-file.js.map +1 -1
  308. package/dist/src/tools/read-file.test.js +29 -0
  309. package/dist/src/tools/read-file.test.js.map +1 -1
  310. package/dist/src/tools/read-many-files.d.ts +1 -1
  311. package/dist/src/tools/read-many-files.js +17 -49
  312. package/dist/src/tools/read-many-files.js.map +1 -1
  313. package/dist/src/tools/ripGrep.d.ts +8 -0
  314. package/dist/src/tools/ripGrep.js +26 -1
  315. package/dist/src/tools/ripGrep.js.map +1 -1
  316. package/dist/src/tools/ripGrep.test.js +107 -5
  317. package/dist/src/tools/ripGrep.test.js.map +1 -1
  318. package/dist/src/tools/shell.d.ts +12 -2
  319. package/dist/src/tools/shell.js +20 -27
  320. package/dist/src/tools/shell.js.map +1 -1
  321. package/dist/src/tools/shell.test.js +33 -68
  322. package/dist/src/tools/shell.test.js.map +1 -1
  323. package/dist/src/tools/smart-edit.d.ts +0 -1
  324. package/dist/src/tools/smart-edit.js +12 -19
  325. package/dist/src/tools/smart-edit.js.map +1 -1
  326. package/dist/src/tools/smart-edit.test.js +68 -9
  327. package/dist/src/tools/smart-edit.test.js.map +1 -1
  328. package/dist/src/tools/tool-registry.js +1 -0
  329. package/dist/src/tools/tool-registry.js.map +1 -1
  330. package/dist/src/tools/tools.d.ts +8 -5
  331. package/dist/src/tools/tools.js +9 -2
  332. package/dist/src/tools/tools.js.map +1 -1
  333. package/dist/src/tools/write-file.js +4 -5
  334. package/dist/src/tools/write-file.js.map +1 -1
  335. package/dist/src/tools/write-file.test.js +94 -10
  336. package/dist/src/tools/write-file.test.js.map +1 -1
  337. package/dist/src/utils/bfsFileSearch.js +11 -5
  338. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  339. package/dist/src/utils/editCorrector.d.ts +7 -6
  340. package/dist/src/utils/editCorrector.js +61 -18
  341. package/dist/src/utils/editCorrector.js.map +1 -1
  342. package/dist/src/utils/editCorrector.test.js +30 -79
  343. package/dist/src/utils/editCorrector.test.js.map +1 -1
  344. package/dist/src/utils/editor.js +31 -44
  345. package/dist/src/utils/editor.js.map +1 -1
  346. package/dist/src/utils/editor.test.js +61 -75
  347. package/dist/src/utils/editor.test.js.map +1 -1
  348. package/dist/src/utils/errorParsing.js +2 -2
  349. package/dist/src/utils/errorParsing.js.map +1 -1
  350. package/dist/src/utils/errorParsing.test.js +7 -7
  351. package/dist/src/utils/errorParsing.test.js.map +1 -1
  352. package/dist/src/utils/errors.d.ts +6 -0
  353. package/dist/src/utils/errors.js +10 -0
  354. package/dist/src/utils/errors.js.map +1 -1
  355. package/dist/src/utils/fileUtils.d.ts +1 -0
  356. package/dist/src/utils/fileUtils.js +10 -0
  357. package/dist/src/utils/fileUtils.js.map +1 -1
  358. package/dist/src/utils/fileUtils.test.js +34 -9
  359. package/dist/src/utils/fileUtils.test.js.map +1 -1
  360. package/dist/src/utils/filesearch/crawler.test.js +1 -1
  361. package/dist/src/utils/filesearch/crawler.test.js.map +1 -1
  362. package/dist/src/utils/filesearch/fileSearch.test.js +1 -1
  363. package/dist/src/utils/filesearch/fileSearch.test.js.map +1 -1
  364. package/dist/src/utils/filesearch/ignore.test.js +1 -1
  365. package/dist/src/utils/filesearch/ignore.test.js.map +1 -1
  366. package/dist/src/utils/flashFallback.test.d.ts +6 -0
  367. package/dist/src/utils/{flashFallback.integration.test.js → flashFallback.test.js} +31 -27
  368. package/dist/src/utils/flashFallback.test.js.map +1 -0
  369. package/dist/src/utils/geminiIgnoreParser.d.ts +18 -0
  370. package/dist/src/utils/geminiIgnoreParser.js +61 -0
  371. package/dist/src/utils/geminiIgnoreParser.js.map +1 -0
  372. package/dist/src/utils/geminiIgnoreParser.test.d.ts +6 -0
  373. package/dist/src/utils/geminiIgnoreParser.test.js +50 -0
  374. package/dist/src/utils/geminiIgnoreParser.test.js.map +1 -0
  375. package/dist/src/utils/gitIgnoreParser.d.ts +3 -8
  376. package/dist/src/utils/gitIgnoreParser.js +60 -60
  377. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  378. package/dist/src/utils/gitIgnoreParser.test.js +18 -53
  379. package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
  380. package/dist/src/utils/installationManager.test.js +1 -1
  381. package/dist/src/utils/installationManager.test.js.map +1 -1
  382. package/dist/src/utils/llm-edit-fixer.d.ts +4 -3
  383. package/dist/src/utils/llm-edit-fixer.js +19 -10
  384. package/dist/src/utils/llm-edit-fixer.js.map +1 -1
  385. package/dist/src/utils/llm-edit-fixer.test.d.ts +6 -0
  386. package/dist/src/utils/llm-edit-fixer.test.js +105 -0
  387. package/dist/src/utils/llm-edit-fixer.test.js.map +1 -0
  388. package/dist/src/utils/memoryDiscovery.test.js +12 -6
  389. package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
  390. package/dist/src/utils/nextSpeakerChecker.d.ts +2 -2
  391. package/dist/src/utils/nextSpeakerChecker.js +8 -2
  392. package/dist/src/utils/nextSpeakerChecker.js.map +1 -1
  393. package/dist/src/utils/nextSpeakerChecker.test.js +52 -74
  394. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
  395. package/dist/src/utils/promptIdContext.d.ts +7 -0
  396. package/dist/src/utils/promptIdContext.js +8 -0
  397. package/dist/src/utils/promptIdContext.js.map +1 -0
  398. package/dist/src/utils/shell-utils.d.ts +5 -0
  399. package/dist/src/utils/shell-utils.js +23 -0
  400. package/dist/src/utils/shell-utils.js.map +1 -1
  401. package/dist/src/utils/terminalSerializer.d.ts +28 -0
  402. package/dist/src/utils/terminalSerializer.js +432 -0
  403. package/dist/src/utils/terminalSerializer.js.map +1 -0
  404. package/dist/src/utils/terminalSerializer.test.d.ts +6 -0
  405. package/dist/src/utils/terminalSerializer.test.js +176 -0
  406. package/dist/src/utils/terminalSerializer.test.js.map +1 -0
  407. package/dist/src/utils/textUtils.d.ts +5 -0
  408. package/dist/src/utils/textUtils.js +14 -0
  409. package/dist/src/utils/textUtils.js.map +1 -1
  410. package/dist/src/utils/textUtils.test.d.ts +6 -0
  411. package/dist/src/utils/textUtils.test.js +59 -0
  412. package/dist/src/utils/textUtils.test.js.map +1 -0
  413. package/dist/src/utils/userAccountManager.test.js +1 -1
  414. package/dist/src/utils/userAccountManager.test.js.map +1 -1
  415. package/dist/tsconfig.tsbuildinfo +1 -1
  416. package/package.json +7 -2
  417. package/dist/src/utils/flashFallback.integration.test.js.map +0 -1
  418. package/dist/src/utils/ide-trust.d.ts +0 -10
  419. package/dist/src/utils/ide-trust.js +0 -14
  420. package/dist/src/utils/ide-trust.js.map +0 -1
  421. /package/dist/src/{utils/flashFallback.integration.test.d.ts → code_assist/oauth-credential-storage.test.d.ts} +0 -0
@@ -4,18 +4,16 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
7
- import { GoogleGenAI } from '@google/genai';
8
7
  import { findIndexAfterFraction, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
9
8
  import { AuthType, } from './contentGenerator.js';
10
9
  import {} from './geminiChat.js';
11
- import { Config } from '../config/config.js';
12
10
  import { CompressionStatus, GeminiEventType, Turn, } from './turn.js';
13
11
  import { getCoreSystemPrompt } from './prompts.js';
14
12
  import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
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';
20
18
  // Mock fs module to prevent actual file system operations during tests
21
19
  const mockFileSystem = new Map();
@@ -41,11 +39,7 @@ vi.mock('node:fs', () => {
41
39
  };
42
40
  });
43
41
  // --- Mocks ---
44
- const mockChatCreateFn = vi.fn();
45
- const mockGenerateContentFn = vi.fn();
46
- const mockEmbedContentFn = vi.fn();
47
42
  const mockTurnRunFn = vi.fn();
48
- vi.mock('@google/genai');
49
43
  vi.mock('./turn', async (importOriginal) => {
50
44
  const actual = await importOriginal();
51
45
  // Define a mock class that has the same shape as the real Turn
@@ -114,22 +108,22 @@ describe('findIndexAfterFraction', () => {
114
108
  // 0: 66
115
109
  // 1: 66 + 68 = 134
116
110
  // 2: 134 + 66 = 200
117
- // 200 >= 166.5, so index is 2
118
- expect(findIndexAfterFraction(history, 0.5)).toBe(2);
111
+ // 200 >= 166.5, so index is 3
112
+ expect(findIndexAfterFraction(history, 0.5)).toBe(3);
119
113
  });
120
114
  it('should handle a fraction that results in the last index', () => {
121
115
  // 333 * 0.9 = 299.7
122
116
  // ...
123
117
  // 3: 200 + 68 = 268
124
118
  // 4: 268 + 65 = 333
125
- // 333 >= 299.7, so index is 4
126
- expect(findIndexAfterFraction(history, 0.9)).toBe(4);
119
+ // 333 >= 299.7, so index is 5
120
+ expect(findIndexAfterFraction(history, 0.9)).toBe(5);
127
121
  });
128
122
  it('should handle an empty history', () => {
129
123
  expect(findIndexAfterFraction([], 0.5)).toBe(0);
130
124
  });
131
125
  it('should handle a history with only one item', () => {
132
- expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(0);
126
+ expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(1);
133
127
  });
134
128
  it('should handle history with weird parts', () => {
135
129
  const historyWithEmptyParts = [
@@ -137,7 +131,7 @@ describe('findIndexAfterFraction', () => {
137
131
  { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
138
132
  { role: 'user', parts: [{ text: 'Message 2' }] },
139
133
  ];
140
- expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(1);
134
+ expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(2);
141
135
  });
142
136
  });
143
137
  describe('isThinkingSupported', () => {
@@ -168,33 +162,23 @@ describe('isThinkingDefault', () => {
168
162
  });
169
163
  });
170
164
  describe('Gemini Client (client.ts)', () => {
165
+ let mockContentGenerator;
166
+ let mockConfig;
171
167
  let client;
168
+ let mockGenerateContentFn;
172
169
  beforeEach(async () => {
173
170
  vi.resetAllMocks();
171
+ mockGenerateContentFn = vi.fn().mockResolvedValue({
172
+ candidates: [{ content: { parts: [{ text: '{"key": "value"}' }] } }],
173
+ });
174
174
  // Disable 429 simulation for tests
175
175
  setSimulate429(false);
176
- // Set up the mock for GoogleGenAI constructor and its methods
177
- const MockedGoogleGenAI = vi.mocked(GoogleGenAI);
178
- MockedGoogleGenAI.mockImplementation(() => {
179
- const mock = {
180
- chats: { create: mockChatCreateFn },
181
- models: {
182
- generateContent: mockGenerateContentFn,
183
- embedContent: mockEmbedContentFn,
184
- },
185
- };
186
- return mock;
187
- });
188
- mockChatCreateFn.mockResolvedValue({});
189
- mockGenerateContentFn.mockResolvedValue({
190
- candidates: [
191
- {
192
- content: {
193
- parts: [{ text: '{"key": "value"}' }],
194
- },
195
- },
196
- ],
197
- });
176
+ mockContentGenerator = {
177
+ generateContent: mockGenerateContentFn,
178
+ generateContentStream: vi.fn(),
179
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
180
+ batchEmbedContents: vi.fn(),
181
+ };
198
182
  // Because the GeminiClient constructor kicks off an async process (startChat)
199
183
  // that depends on a fully-formed Config object, we need to mock the
200
184
  // entire implementation of Config for these tests.
@@ -204,12 +188,11 @@ describe('Gemini Client (client.ts)', () => {
204
188
  };
205
189
  const fileService = new FileDiscoveryService('/test/dir');
206
190
  const contentGeneratorConfig = {
207
- model: 'test-model',
208
191
  apiKey: 'test-key',
209
192
  vertexai: false,
210
193
  authType: AuthType.USE_GEMINI,
211
194
  };
212
- const mockConfigObject = {
195
+ mockConfig = {
213
196
  getContentGeneratorConfig: vi
214
197
  .fn()
215
198
  .mockReturnValue(contentGeneratorConfig),
@@ -237,164 +220,34 @@ describe('Gemini Client (client.ts)', () => {
237
220
  getDirectories: vi.fn().mockReturnValue(['/test/dir']),
238
221
  }),
239
222
  getGeminiClient: vi.fn(),
223
+ getModelRouterService: vi.fn().mockReturnValue({
224
+ route: vi.fn().mockResolvedValue({ model: 'default-routed-model' }),
225
+ }),
226
+ isInFallbackMode: vi.fn().mockReturnValue(false),
240
227
  setFallbackMode: vi.fn(),
241
228
  getChatCompression: vi.fn().mockReturnValue(undefined),
242
229
  getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
243
230
  getUseSmartEdit: vi.fn().mockReturnValue(false),
231
+ getUseModelRouter: vi.fn().mockReturnValue(false),
244
232
  getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
245
233
  storage: {
246
234
  getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
247
235
  },
236
+ getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
237
+ getBaseLlmClient: vi.fn().mockReturnValue({
238
+ generateJson: vi.fn().mockResolvedValue({
239
+ next_speaker: 'user',
240
+ reasoning: 'test',
241
+ }),
242
+ }),
248
243
  };
249
- const MockedConfig = vi.mocked(Config, true);
250
- MockedConfig.mockImplementation(() => mockConfigObject);
251
- // We can instantiate the client here since Config is mocked
252
- // and the constructor will use the mocked GoogleGenAI
253
- client = new GeminiClient(new Config({ sessionId: 'test-session-id' }));
254
- mockConfigObject.getGeminiClient.mockReturnValue(client);
255
- await client.initialize(contentGeneratorConfig);
244
+ client = new GeminiClient(mockConfig);
245
+ await client.initialize();
246
+ vi.mocked(mockConfig.getGeminiClient).mockReturnValue(client);
256
247
  });
257
248
  afterEach(() => {
258
249
  vi.restoreAllMocks();
259
250
  });
260
- // NOTE: The following tests for startChat were removed due to persistent issues with
261
- // the @google/genai mock. Specifically, the mockChatCreateFn (representing instance.chats.create)
262
- // was not being detected as called by the GeminiClient instance.
263
- // This likely points to a subtle issue in how the GoogleGenerativeAI class constructor
264
- // and its instance methods are mocked and then used by the class under test.
265
- // For future debugging, ensure that the `this.client` in `GeminiClient` (which is an
266
- // instance of the mocked GoogleGenerativeAI) correctly has its `chats.create` method
267
- // pointing to `mockChatCreateFn`.
268
- // it('startChat should call getCoreSystemPrompt with userMemory and pass to chats.create', async () => { ... });
269
- // it('startChat should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
270
- // NOTE: The following tests for generateJson were removed due to persistent issues with
271
- // the @google/genai mock, similar to the startChat tests. The mockGenerateContentFn
272
- // (representing instance.models.generateContent) was not being detected as called, or the mock
273
- // was not preventing an actual API call (leading to API key errors).
274
- // For future debugging, ensure `this.client.models.generateContent` in `GeminiClient` correctly
275
- // uses the `mockGenerateContentFn`.
276
- // it('generateJson should call getCoreSystemPrompt with userMemory and pass to generateContent', async () => { ... });
277
- // it('generateJson should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
278
- describe('generateEmbedding', () => {
279
- const texts = ['hello world', 'goodbye world'];
280
- const testEmbeddingModel = 'test-embedding-model';
281
- it('should call embedContent with correct parameters and return embeddings', async () => {
282
- const mockEmbeddings = [
283
- [0.1, 0.2, 0.3],
284
- [0.4, 0.5, 0.6],
285
- ];
286
- const mockResponse = {
287
- embeddings: [
288
- { values: mockEmbeddings[0] },
289
- { values: mockEmbeddings[1] },
290
- ],
291
- };
292
- mockEmbedContentFn.mockResolvedValue(mockResponse);
293
- const result = await client.generateEmbedding(texts);
294
- expect(mockEmbedContentFn).toHaveBeenCalledTimes(1);
295
- expect(mockEmbedContentFn).toHaveBeenCalledWith({
296
- model: testEmbeddingModel,
297
- contents: texts,
298
- });
299
- expect(result).toEqual(mockEmbeddings);
300
- });
301
- it('should return an empty array if an empty array is passed', async () => {
302
- const result = await client.generateEmbedding([]);
303
- expect(result).toEqual([]);
304
- expect(mockEmbedContentFn).not.toHaveBeenCalled();
305
- });
306
- it('should throw an error if API response has no embeddings array', async () => {
307
- mockEmbedContentFn.mockResolvedValue({}); // No `embeddings` key
308
- await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
309
- });
310
- it('should throw an error if API response has an empty embeddings array', async () => {
311
- const mockResponse = {
312
- embeddings: [],
313
- };
314
- mockEmbedContentFn.mockResolvedValue(mockResponse);
315
- await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
316
- });
317
- it('should throw an error if API returns a mismatched number of embeddings', async () => {
318
- const mockResponse = {
319
- embeddings: [{ values: [1, 2, 3] }], // Only one for two texts
320
- };
321
- mockEmbedContentFn.mockResolvedValue(mockResponse);
322
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned a mismatched number of embeddings. Expected 2, got 1.');
323
- });
324
- it('should throw an error if any embedding has nullish values', async () => {
325
- const mockResponse = {
326
- embeddings: [{ values: [1, 2, 3] }, { values: undefined }], // Second one is bad
327
- };
328
- mockEmbedContentFn.mockResolvedValue(mockResponse);
329
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 1: "goodbye world"');
330
- });
331
- it('should throw an error if any embedding has an empty values array', async () => {
332
- const mockResponse = {
333
- embeddings: [{ values: [] }, { values: [1, 2, 3] }], // First one is bad
334
- };
335
- mockEmbedContentFn.mockResolvedValue(mockResponse);
336
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 0: "hello world"');
337
- });
338
- it('should propagate errors from the API call', async () => {
339
- const apiError = new Error('API Failure');
340
- mockEmbedContentFn.mockRejectedValue(apiError);
341
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API Failure');
342
- });
343
- });
344
- describe('generateJson', () => {
345
- it('should call generateContent with the correct parameters', async () => {
346
- const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
347
- const schema = { type: 'string' };
348
- const abortSignal = new AbortController().signal;
349
- // Mock countTokens
350
- const mockGenerator = {
351
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
352
- generateContent: mockGenerateContentFn,
353
- };
354
- client['contentGenerator'] = mockGenerator;
355
- await client.generateJson(contents, schema, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
356
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
357
- model: DEFAULT_GEMINI_FLASH_MODEL,
358
- config: {
359
- abortSignal,
360
- systemInstruction: getCoreSystemPrompt(''),
361
- temperature: 0,
362
- topP: 1,
363
- responseJsonSchema: schema,
364
- responseMimeType: 'application/json',
365
- },
366
- contents,
367
- }, 'test-session-id');
368
- });
369
- it('should allow overriding model and config', async () => {
370
- const contents = [
371
- { role: 'user', parts: [{ text: 'hello' }] },
372
- ];
373
- const schema = { type: 'string' };
374
- const abortSignal = new AbortController().signal;
375
- const customModel = 'custom-json-model';
376
- const customConfig = { temperature: 0.9, topK: 20 };
377
- const mockGenerator = {
378
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
379
- generateContent: mockGenerateContentFn,
380
- };
381
- client['contentGenerator'] = mockGenerator;
382
- await client.generateJson(contents, schema, abortSignal, customModel, customConfig);
383
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
384
- model: customModel,
385
- config: {
386
- abortSignal,
387
- systemInstruction: getCoreSystemPrompt(''),
388
- temperature: 0.9,
389
- topP: 1, // from default
390
- topK: 20,
391
- responseJsonSchema: schema,
392
- responseMimeType: 'application/json',
393
- },
394
- contents,
395
- }, 'test-session-id');
396
- });
397
- });
398
251
  describe('addHistory', () => {
399
252
  it('should call chat.addHistory with the provided content', async () => {
400
253
  const mockChat = {
@@ -432,21 +285,15 @@ describe('Gemini Client (client.ts)', () => {
432
285
  });
433
286
  });
434
287
  describe('tryCompressChat', () => {
435
- const mockCountTokens = vi.fn();
436
- const mockSendMessage = vi.fn();
437
288
  const mockGetHistory = vi.fn();
438
289
  beforeEach(() => {
439
290
  vi.mock('./tokenLimits', () => ({
440
291
  tokenLimit: vi.fn(),
441
292
  }));
442
- client['contentGenerator'] = {
443
- countTokens: mockCountTokens,
444
- };
445
293
  client['chat'] = {
446
294
  getHistory: mockGetHistory,
447
295
  addHistory: vi.fn(),
448
296
  setHistory: vi.fn(),
449
- sendMessage: mockSendMessage,
450
297
  };
451
298
  });
452
299
  function setup({ chatHistory = [
@@ -456,28 +303,21 @@ describe('Gemini Client (client.ts)', () => {
456
303
  const mockChat = {
457
304
  getHistory: vi.fn().mockReturnValue(chatHistory),
458
305
  setHistory: vi.fn(),
459
- sendMessage: vi.fn().mockResolvedValue({ text: 'Summary' }),
460
306
  };
461
- const mockCountTokens = vi
462
- .fn()
307
+ vi.mocked(mockContentGenerator.countTokens)
463
308
  .mockResolvedValueOnce({ totalTokens: 1000 })
464
309
  .mockResolvedValueOnce({ totalTokens: 5000 });
465
- const mockGenerator = {
466
- countTokens: mockCountTokens,
467
- };
468
310
  client['chat'] = mockChat;
469
- client['contentGenerator'] = mockGenerator;
470
311
  client['startChat'] = vi.fn().mockResolvedValue({ ...mockChat });
471
- return { client, mockChat, mockGenerator };
312
+ return { client, mockChat };
472
313
  }
473
314
  describe('when compression inflates the token count', () => {
474
- it('uses the truncated history for compression');
475
315
  it('allows compression to be forced/manual after a failure', async () => {
476
- const { client, mockGenerator } = setup();
477
- mockGenerator.countTokens?.mockResolvedValue({
316
+ const { client } = setup();
317
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
478
318
  totalTokens: 1000,
479
319
  });
480
- await client.tryCompressChat('prompt-id-4'); // Fails
320
+ await client.tryCompressChat('prompt-id-4', false); // Fails
481
321
  const result = await client.tryCompressChat('prompt-id-4', true);
482
322
  expect(result).toEqual({
483
323
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -487,7 +327,10 @@ describe('Gemini Client (client.ts)', () => {
487
327
  });
488
328
  it('yields the result even if the compression inflated the tokens', async () => {
489
329
  const { client } = setup();
490
- const result = await client.tryCompressChat('prompt-id-4', true);
330
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
331
+ totalTokens: 1000,
332
+ });
333
+ const result = await client.tryCompressChat('prompt-id-4', false);
491
334
  expect(result).toEqual({
492
335
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
493
336
  newTokenCount: 5000,
@@ -496,12 +339,12 @@ describe('Gemini Client (client.ts)', () => {
496
339
  });
497
340
  it('does not manipulate the source chat', async () => {
498
341
  const { client, mockChat } = setup();
499
- await client.tryCompressChat('prompt-id-4', true);
342
+ await client.tryCompressChat('prompt-id-4', false);
500
343
  expect(client['chat']).toBe(mockChat); // a new chat session was not created
501
344
  });
502
345
  it('restores the history back to the original', async () => {
503
346
  vi.mocked(tokenLimit).mockReturnValue(1000);
504
- mockCountTokens.mockResolvedValue({
347
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
505
348
  totalTokens: 999,
506
349
  });
507
350
  const originalHistory = [
@@ -512,16 +355,16 @@ describe('Gemini Client (client.ts)', () => {
512
355
  const { client } = setup({
513
356
  chatHistory: originalHistory,
514
357
  });
515
- const { compressionStatus } = await client.tryCompressChat('prompt-id-4');
358
+ const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false);
516
359
  expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
517
360
  expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
518
361
  });
519
362
  it('will not attempt to compress context after a failure', async () => {
520
- const { client, mockGenerator } = setup();
521
- await client.tryCompressChat('prompt-id-4');
522
- const result = await client.tryCompressChat('prompt-id-5');
363
+ const { client } = setup();
364
+ await client.tryCompressChat('prompt-id-4', false);
365
+ const result = await client.tryCompressChat('prompt-id-5', false);
523
366
  // it counts tokens for {original, compressed} and then never again
524
- expect(mockGenerator.countTokens).toHaveBeenCalledTimes(2);
367
+ expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
525
368
  expect(result).toEqual({
526
369
  compressionStatus: CompressionStatus.NOOP,
527
370
  newTokenCount: 0,
@@ -529,37 +372,17 @@ describe('Gemini Client (client.ts)', () => {
529
372
  });
530
373
  });
531
374
  });
532
- it('attempts to compress with a maxOutputTokens set to the original token count', async () => {
533
- vi.mocked(tokenLimit).mockReturnValue(1000);
534
- mockCountTokens.mockResolvedValue({
535
- totalTokens: 999,
536
- });
537
- mockGetHistory.mockReturnValue([
538
- { role: 'user', parts: [{ text: '...history...' }] },
539
- ]);
540
- // Mock the summary response from the chat
541
- mockSendMessage.mockResolvedValue({
542
- role: 'model',
543
- parts: [{ text: 'This is a summary.' }],
544
- });
545
- await client.tryCompressChat('prompt-id-2', true);
546
- expect(mockSendMessage).toHaveBeenCalledWith(expect.objectContaining({
547
- config: expect.objectContaining({
548
- maxOutputTokens: 999,
549
- }),
550
- }), 'prompt-id-2');
551
- });
552
375
  it('should not trigger summarization if token count is below threshold', async () => {
553
376
  const MOCKED_TOKEN_LIMIT = 1000;
554
377
  vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
555
378
  mockGetHistory.mockReturnValue([
556
379
  { role: 'user', parts: [{ text: '...history...' }] },
557
380
  ]);
558
- mockCountTokens.mockResolvedValue({
381
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
559
382
  totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
560
383
  });
561
384
  const initialChat = client.getChat();
562
- const result = await client.tryCompressChat('prompt-id-2');
385
+ const result = await client.tryCompressChat('prompt-id-2', false);
563
386
  const newChat = client.getChat();
564
387
  expect(tokenLimit).toHaveBeenCalled();
565
388
  expect(result).toEqual({
@@ -582,15 +405,21 @@ describe('Gemini Client (client.ts)', () => {
582
405
  ]);
583
406
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
584
407
  const newTokenCount = 100;
585
- mockCountTokens
408
+ vi.mocked(mockContentGenerator.countTokens)
586
409
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
587
410
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
588
411
  // Mock the summary response from the chat
589
- mockSendMessage.mockResolvedValue({
590
- role: 'model',
591
- parts: [{ text: 'This is a summary.' }],
412
+ mockGenerateContentFn.mockResolvedValue({
413
+ candidates: [
414
+ {
415
+ content: {
416
+ role: 'model',
417
+ parts: [{ text: 'This is a summary.' }],
418
+ },
419
+ },
420
+ ],
592
421
  });
593
- await client.tryCompressChat('prompt-id-3');
422
+ await client.tryCompressChat('prompt-id-3', false);
594
423
  expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
595
424
  tokens_before: originalTokenCount,
596
425
  tokens_after: newTokenCount,
@@ -608,19 +437,25 @@ describe('Gemini Client (client.ts)', () => {
608
437
  ]);
609
438
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
610
439
  const newTokenCount = 100;
611
- mockCountTokens
440
+ vi.mocked(mockContentGenerator.countTokens)
612
441
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
613
442
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
614
443
  // Mock the summary response from the chat
615
- mockSendMessage.mockResolvedValue({
616
- role: 'model',
617
- parts: [{ text: 'This is a summary.' }],
444
+ mockGenerateContentFn.mockResolvedValue({
445
+ candidates: [
446
+ {
447
+ content: {
448
+ role: 'model',
449
+ parts: [{ text: 'This is a summary.' }],
450
+ },
451
+ },
452
+ ],
618
453
  });
619
454
  const initialChat = client.getChat();
620
- const result = await client.tryCompressChat('prompt-id-3');
455
+ const result = await client.tryCompressChat('prompt-id-3', false);
621
456
  const newChat = client.getChat();
622
457
  expect(tokenLimit).toHaveBeenCalled();
623
- expect(mockSendMessage).toHaveBeenCalled();
458
+ expect(mockGenerateContentFn).toHaveBeenCalled();
624
459
  // Assert that summarization happened and returned the correct stats
625
460
  expect(result).toEqual({
626
461
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -653,19 +488,25 @@ describe('Gemini Client (client.ts)', () => {
653
488
  ]);
654
489
  const originalTokenCount = 1000 * 0.7;
655
490
  const newTokenCount = 100;
656
- mockCountTokens
491
+ vi.mocked(mockContentGenerator.countTokens)
657
492
  .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
658
493
  .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
659
494
  // Mock the summary response from the chat
660
- mockSendMessage.mockResolvedValue({
661
- role: 'model',
662
- parts: [{ text: 'This is a summary.' }],
495
+ mockGenerateContentFn.mockResolvedValue({
496
+ candidates: [
497
+ {
498
+ content: {
499
+ role: 'model',
500
+ parts: [{ text: 'This is a summary.' }],
501
+ },
502
+ },
503
+ ],
663
504
  });
664
505
  const initialChat = client.getChat();
665
- const result = await client.tryCompressChat('prompt-id-3');
506
+ const result = await client.tryCompressChat('prompt-id-3', false);
666
507
  const newChat = client.getChat();
667
508
  expect(tokenLimit).toHaveBeenCalled();
668
- expect(mockSendMessage).toHaveBeenCalled();
509
+ expect(mockGenerateContentFn).toHaveBeenCalled();
669
510
  // Assert that summarization happened and returned the correct stats
670
511
  expect(result).toEqual({
671
512
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -687,18 +528,24 @@ describe('Gemini Client (client.ts)', () => {
687
528
  ]);
688
529
  const originalTokenCount = 10; // Well below threshold
689
530
  const newTokenCount = 5;
690
- mockCountTokens
531
+ vi.mocked(mockContentGenerator.countTokens)
691
532
  .mockResolvedValueOnce({ totalTokens: originalTokenCount })
692
533
  .mockResolvedValueOnce({ totalTokens: newTokenCount });
693
534
  // Mock the summary response from the chat
694
- mockSendMessage.mockResolvedValue({
695
- role: 'model',
696
- parts: [{ text: 'This is a summary.' }],
535
+ mockGenerateContentFn.mockResolvedValue({
536
+ candidates: [
537
+ {
538
+ content: {
539
+ role: 'model',
540
+ parts: [{ text: 'This is a summary.' }],
541
+ },
542
+ },
543
+ ],
697
544
  });
698
545
  const initialChat = client.getChat();
699
- const result = await client.tryCompressChat('prompt-id-1', true); // force = true
546
+ const result = await client.tryCompressChat('prompt-id-1', false); // force = true
700
547
  const newChat = client.getChat();
701
- expect(mockSendMessage).toHaveBeenCalled();
548
+ expect(mockGenerateContentFn).toHaveBeenCalled();
702
549
  expect(result).toEqual({
703
550
  compressionStatus: CompressionStatus.COMPRESSED,
704
551
  originalTokenCount,
@@ -707,68 +554,13 @@ describe('Gemini Client (client.ts)', () => {
707
554
  // Assert that the chat was reset
708
555
  expect(newChat).not.toBe(initialChat);
709
556
  });
710
- it('should use current model from config for token counting after sendMessage', async () => {
711
- const initialModel = client['config'].getModel();
712
- const mockCountTokens = vi
713
- .fn()
714
- .mockResolvedValueOnce({ totalTokens: 100000 })
715
- .mockResolvedValueOnce({ totalTokens: 5000 });
716
- const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
717
- const mockChatHistory = [
718
- { role: 'user', parts: [{ text: 'Long conversation' }] },
719
- { role: 'model', parts: [{ text: 'Long response' }] },
720
- ];
721
- const mockChat = {
722
- getHistory: vi.fn().mockReturnValue(mockChatHistory),
723
- setHistory: vi.fn(),
724
- sendMessage: mockSendMessage,
725
- };
726
- const mockGenerator = {
727
- countTokens: mockCountTokens,
728
- };
729
- // mock the model has been changed between calls of `countTokens`
730
- const firstCurrentModel = initialModel + '-changed-1';
731
- const secondCurrentModel = initialModel + '-changed-2';
732
- vi.spyOn(client['config'], 'getModel')
733
- .mockReturnValueOnce(firstCurrentModel)
734
- .mockReturnValueOnce(secondCurrentModel);
735
- client['chat'] = mockChat;
736
- client['contentGenerator'] = mockGenerator;
737
- client['startChat'] = vi.fn().mockResolvedValue(mockChat);
738
- const result = await client.tryCompressChat('prompt-id-4', true);
739
- expect(mockCountTokens).toHaveBeenCalledTimes(2);
740
- expect(mockCountTokens).toHaveBeenNthCalledWith(1, {
741
- model: firstCurrentModel,
742
- contents: mockChatHistory,
743
- });
744
- expect(mockCountTokens).toHaveBeenNthCalledWith(2, {
745
- model: secondCurrentModel,
746
- contents: expect.any(Array),
747
- });
748
- expect(result).toEqual({
749
- compressionStatus: CompressionStatus.COMPRESSED,
750
- originalTokenCount: 100000,
751
- newTokenCount: 5000,
752
- });
753
- });
754
557
  });
755
558
  describe('sendMessageStream', () => {
756
559
  it('emits a compression event when the context was automatically compressed', async () => {
757
560
  // Arrange
758
- const mockStream = (async function* () {
561
+ mockTurnRunFn.mockReturnValue((async function* () {
759
562
  yield { type: 'content', value: 'Hello' };
760
- })();
761
- mockTurnRunFn.mockReturnValue(mockStream);
762
- const mockChat = {
763
- addHistory: vi.fn(),
764
- getHistory: vi.fn().mockReturnValue([]),
765
- };
766
- client['chat'] = mockChat;
767
- const mockGenerator = {
768
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
769
- generateContent: mockGenerateContentFn,
770
- };
771
- client['contentGenerator'] = mockGenerator;
563
+ })());
772
564
  const compressionInfo = {
773
565
  compressionStatus: CompressionStatus.COMPRESSED,
774
566
  originalTokenCount: 1000,
@@ -798,16 +590,6 @@ describe('Gemini Client (client.ts)', () => {
798
590
  yield { type: 'content', value: 'Hello' };
799
591
  })();
800
592
  mockTurnRunFn.mockReturnValue(mockStream);
801
- const mockChat = {
802
- addHistory: vi.fn(),
803
- getHistory: vi.fn().mockReturnValue([]),
804
- };
805
- client['chat'] = mockChat;
806
- const mockGenerator = {
807
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
808
- generateContent: mockGenerateContentFn,
809
- };
810
- client['contentGenerator'] = mockGenerator;
811
593
  const compressionInfo = {
812
594
  compressionStatus,
813
595
  originalTokenCount: 1000,
@@ -825,7 +607,7 @@ describe('Gemini Client (client.ts)', () => {
825
607
  });
826
608
  it('should include editor context when ideMode is enabled', async () => {
827
609
  // Arrange
828
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
610
+ vi.mocked(ideContextStore.get).mockReturnValue({
829
611
  workspaceState: {
830
612
  openFiles: [
831
613
  {
@@ -846,21 +628,20 @@ describe('Gemini Client (client.ts)', () => {
846
628
  ],
847
629
  },
848
630
  });
849
- vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
850
- const mockStream = (async function* () {
631
+ vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
632
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
633
+ originalTokenCount: 0,
634
+ newTokenCount: 0,
635
+ compressionStatus: CompressionStatus.COMPRESSED,
636
+ });
637
+ mockTurnRunFn.mockReturnValue((async function* () {
851
638
  yield { type: 'content', value: 'Hello' };
852
- })();
853
- mockTurnRunFn.mockReturnValue(mockStream);
639
+ })());
854
640
  const mockChat = {
855
641
  addHistory: vi.fn(),
856
642
  getHistory: vi.fn().mockReturnValue([]),
857
643
  };
858
644
  client['chat'] = mockChat;
859
- const mockGenerator = {
860
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
861
- generateContent: mockGenerateContentFn,
862
- };
863
- client['contentGenerator'] = mockGenerator;
864
645
  const initialRequest = [{ text: 'Hi' }];
865
646
  // Act
866
647
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -868,7 +649,7 @@ describe('Gemini Client (client.ts)', () => {
868
649
  // consume stream
869
650
  }
870
651
  // Assert
871
- expect(ideContext.getIdeContext).toHaveBeenCalled();
652
+ expect(ideContextStore.get).toHaveBeenCalled();
872
653
  const expectedContext = `
873
654
  Here is the user's editor context as a JSON object. This is for your information only.
874
655
  \`\`\`json
@@ -893,7 +674,7 @@ ${JSON.stringify({
893
674
  });
894
675
  it('should not add context if ideMode is enabled but no open files', async () => {
895
676
  // Arrange
896
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
677
+ vi.mocked(ideContextStore.get).mockReturnValue({
897
678
  workspaceState: {
898
679
  openFiles: [],
899
680
  },
@@ -908,11 +689,6 @@ ${JSON.stringify({
908
689
  getHistory: vi.fn().mockReturnValue([]),
909
690
  };
910
691
  client['chat'] = mockChat;
911
- const mockGenerator = {
912
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
913
- generateContent: mockGenerateContentFn,
914
- };
915
- client['contentGenerator'] = mockGenerator;
916
692
  const initialRequest = [{ text: 'Hi' }];
917
693
  // Act
918
694
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -920,12 +696,16 @@ ${JSON.stringify({
920
696
  // consume stream
921
697
  }
922
698
  // Assert
923
- expect(ideContext.getIdeContext).toHaveBeenCalled();
924
- expect(mockTurnRunFn).toHaveBeenCalledWith(initialRequest, expect.any(Object));
699
+ expect(ideContextStore.get).toHaveBeenCalled();
700
+ // The `turn.run` method is now called with the model name as the first
701
+ // argument. We use `expect.any(String)` because this test is
702
+ // concerned with the IDE context logic, not the model routing,
703
+ // which is tested in its own dedicated suite.
704
+ expect(mockTurnRunFn).toHaveBeenCalledWith(expect.any(String), initialRequest, expect.any(Object));
925
705
  });
926
706
  it('should add context if ideMode is enabled and there is one active file', async () => {
927
707
  // Arrange
928
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
708
+ vi.mocked(ideContextStore.get).mockReturnValue({
929
709
  workspaceState: {
930
710
  openFiles: [
931
711
  {
@@ -939,6 +719,11 @@ ${JSON.stringify({
939
719
  },
940
720
  });
941
721
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
722
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
723
+ originalTokenCount: 0,
724
+ newTokenCount: 0,
725
+ compressionStatus: CompressionStatus.COMPRESSED,
726
+ });
942
727
  const mockStream = (async function* () {
943
728
  yield { type: 'content', value: 'Hello' };
944
729
  })();
@@ -948,11 +733,6 @@ ${JSON.stringify({
948
733
  getHistory: vi.fn().mockReturnValue([]),
949
734
  };
950
735
  client['chat'] = mockChat;
951
- const mockGenerator = {
952
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
953
- generateContent: mockGenerateContentFn,
954
- };
955
- client['contentGenerator'] = mockGenerator;
956
736
  const initialRequest = [{ text: 'Hi' }];
957
737
  // Act
958
738
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -960,7 +740,7 @@ ${JSON.stringify({
960
740
  // consume stream
961
741
  }
962
742
  // Assert
963
- expect(ideContext.getIdeContext).toHaveBeenCalled();
743
+ expect(ideContextStore.get).toHaveBeenCalled();
964
744
  const expectedContext = `
965
745
  Here is the user's editor context as a JSON object. This is for your information only.
966
746
  \`\`\`json
@@ -984,7 +764,7 @@ ${JSON.stringify({
984
764
  });
985
765
  it('should add context if ideMode is enabled and there are open files but no active file', async () => {
986
766
  // Arrange
987
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
767
+ vi.mocked(ideContextStore.get).mockReturnValue({
988
768
  workspaceState: {
989
769
  openFiles: [
990
770
  {
@@ -999,6 +779,11 @@ ${JSON.stringify({
999
779
  },
1000
780
  });
1001
781
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
782
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
783
+ originalTokenCount: 0,
784
+ newTokenCount: 0,
785
+ compressionStatus: CompressionStatus.COMPRESSED,
786
+ });
1002
787
  const mockStream = (async function* () {
1003
788
  yield { type: 'content', value: 'Hello' };
1004
789
  })();
@@ -1008,11 +793,6 @@ ${JSON.stringify({
1008
793
  getHistory: vi.fn().mockReturnValue([]),
1009
794
  };
1010
795
  client['chat'] = mockChat;
1011
- const mockGenerator = {
1012
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1013
- generateContent: mockGenerateContentFn,
1014
- };
1015
- client['contentGenerator'] = mockGenerator;
1016
796
  const initialRequest = [{ text: 'Hi' }];
1017
797
  // Act
1018
798
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -1020,7 +800,7 @@ ${JSON.stringify({
1020
800
  // consume stream
1021
801
  }
1022
802
  // Assert
1023
- expect(ideContext.getIdeContext).toHaveBeenCalled();
803
+ expect(ideContextStore.get).toHaveBeenCalled();
1024
804
  const expectedContext = `
1025
805
  Here is the user's editor context as a JSON object. This is for your information only.
1026
806
  \`\`\`json
@@ -1046,11 +826,6 @@ ${JSON.stringify({
1046
826
  getHistory: vi.fn().mockReturnValue([]),
1047
827
  };
1048
828
  client['chat'] = mockChat;
1049
- const mockGenerator = {
1050
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1051
- generateContent: mockGenerateContentFn,
1052
- };
1053
- client['contentGenerator'] = mockGenerator;
1054
829
  // Act
1055
830
  const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
1056
831
  // Consume the stream manually to get the final return value.
@@ -1083,11 +858,6 @@ ${JSON.stringify({
1083
858
  getHistory: vi.fn().mockReturnValue([]),
1084
859
  };
1085
860
  client['chat'] = mockChat;
1086
- const mockGenerator = {
1087
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1088
- generateContent: mockGenerateContentFn,
1089
- };
1090
- client['contentGenerator'] = mockGenerator;
1091
861
  // Use a signal that never gets aborted
1092
862
  const abortController = new AbortController();
1093
863
  const signal = abortController.signal;
@@ -1150,11 +920,6 @@ ${JSON.stringify({
1150
920
  getHistory: vi.fn().mockReturnValue([]),
1151
921
  };
1152
922
  client['chat'] = mockChat;
1153
- const mockGenerator = {
1154
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1155
- generateContent: mockGenerateContentFn,
1156
- };
1157
- client['contentGenerator'] = mockGenerator;
1158
923
  // Act & Assert
1159
924
  // Run up to the limit
1160
925
  for (let i = 0; i < MAX_SESSION_TURNS; i++) {
@@ -1193,11 +958,6 @@ ${JSON.stringify({
1193
958
  getHistory: vi.fn().mockReturnValue([]),
1194
959
  };
1195
960
  client['chat'] = mockChat;
1196
- const mockGenerator = {
1197
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1198
- generateContent: mockGenerateContentFn,
1199
- };
1200
- client['contentGenerator'] = mockGenerator;
1201
961
  // Use a signal that never gets aborted
1202
962
  const abortController = new AbortController();
1203
963
  const signal = abortController.signal;
@@ -1236,6 +996,91 @@ ${JSON.stringify({
1236
996
  console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
1237
997
  `${eventCount} events generated (properly bounded by MAX_TURNS)`);
1238
998
  });
999
+ describe('Model Routing', () => {
1000
+ let mockRouterService;
1001
+ beforeEach(() => {
1002
+ mockRouterService = {
1003
+ route: vi
1004
+ .fn()
1005
+ .mockResolvedValue({ model: 'routed-model', reason: 'test' }),
1006
+ };
1007
+ vi.mocked(mockConfig.getModelRouterService).mockReturnValue(mockRouterService);
1008
+ mockTurnRunFn.mockReturnValue((async function* () {
1009
+ yield { type: 'content', value: 'Hello' };
1010
+ })());
1011
+ });
1012
+ it('should use the model router service to select a model on the first turn', async () => {
1013
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1014
+ await fromAsync(stream); // consume stream
1015
+ expect(mockConfig.getModelRouterService).toHaveBeenCalled();
1016
+ expect(mockRouterService.route).toHaveBeenCalled();
1017
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', // The model from the router
1018
+ [{ text: 'Hi' }], expect.any(Object));
1019
+ });
1020
+ it('should use the same model for subsequent turns in the same prompt (stickiness)', async () => {
1021
+ // First turn
1022
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1023
+ await fromAsync(stream);
1024
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1025
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1026
+ // Second turn
1027
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-1');
1028
+ await fromAsync(stream);
1029
+ // Router should not be called again
1030
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1031
+ // Should stick to the first model
1032
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Continue' }], expect.any(Object));
1033
+ });
1034
+ it('should reset the sticky model and re-route when the prompt_id changes', async () => {
1035
+ // First prompt
1036
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1037
+ await fromAsync(stream);
1038
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1039
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1040
+ // New prompt
1041
+ mockRouterService.route.mockResolvedValue({
1042
+ model: 'new-routed-model',
1043
+ reason: 'test',
1044
+ });
1045
+ stream = client.sendMessageStream([{ text: 'A new topic' }], new AbortController().signal, 'prompt-2');
1046
+ await fromAsync(stream);
1047
+ // Router should be called again for the new prompt
1048
+ expect(mockRouterService.route).toHaveBeenCalledTimes(2);
1049
+ // Should use the newly routed model
1050
+ expect(mockTurnRunFn).toHaveBeenCalledWith('new-routed-model', [{ text: 'A new topic' }], expect.any(Object));
1051
+ });
1052
+ it('should use the fallback model and bypass routing when in fallback mode', async () => {
1053
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1054
+ mockRouterService.route.mockResolvedValue({
1055
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1056
+ reason: 'fallback',
1057
+ });
1058
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1059
+ await fromAsync(stream);
1060
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1061
+ });
1062
+ it('should stick to the fallback model for the entire sequence even if fallback mode ends', async () => {
1063
+ // Start the sequence in fallback mode
1064
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1065
+ mockRouterService.route.mockResolvedValue({
1066
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1067
+ reason: 'fallback',
1068
+ });
1069
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-fallback-stickiness');
1070
+ await fromAsync(stream);
1071
+ // First call should use fallback model
1072
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1073
+ // End fallback mode
1074
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(false);
1075
+ // Second call in the same sequence
1076
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-fallback-stickiness');
1077
+ await fromAsync(stream);
1078
+ // Router should still not be called, and it should stick to the fallback model
1079
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(2); // Ensure it was called again
1080
+ expect(mockTurnRunFn).toHaveBeenLastCalledWith(DEFAULT_GEMINI_FLASH_MODEL, // Still the fallback model
1081
+ [{ text: 'Continue' }], expect.any(Object));
1082
+ });
1083
+ });
1239
1084
  describe('Editor context delta', () => {
1240
1085
  const mockStream = (async function* () {
1241
1086
  yield { type: 'content', value: 'Hello' };
@@ -1252,7 +1097,6 @@ ${JSON.stringify({
1252
1097
  const mockChat = {
1253
1098
  addHistory: vi.fn(),
1254
1099
  setHistory: vi.fn(),
1255
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1256
1100
  // Assume history is not empty for delta checks
1257
1101
  getHistory: vi
1258
1102
  .fn()
@@ -1261,11 +1105,6 @@ ${JSON.stringify({
1261
1105
  ]),
1262
1106
  };
1263
1107
  client['chat'] = mockChat;
1264
- const mockGenerator = {
1265
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1266
- generateContent: mockGenerateContentFn,
1267
- };
1268
- client['contentGenerator'] = mockGenerator;
1269
1108
  });
1270
1109
  const testCases = [
1271
1110
  {
@@ -1381,7 +1220,7 @@ ${JSON.stringify({
1381
1220
  },
1382
1221
  };
1383
1222
  // Setup current context
1384
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1223
+ vi.mocked(ideContextStore.get).mockReturnValue({
1385
1224
  workspaceState: {
1386
1225
  openFiles: [
1387
1226
  { ...currentActiveFile, isActive: true, timestamp: Date.now() },
@@ -1427,7 +1266,7 @@ ${JSON.stringify({
1427
1266
  },
1428
1267
  };
1429
1268
  // Setup current context (same as previous)
1430
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1269
+ vi.mocked(ideContextStore.get).mockReturnValue({
1431
1270
  workspaceState: {
1432
1271
  openFiles: [
1433
1272
  { ...activeFile, isActive: true, timestamp: Date.now() },
@@ -1472,15 +1311,10 @@ ${JSON.stringify({
1472
1311
  addHistory: vi.fn(),
1473
1312
  getHistory: vi.fn().mockReturnValue([]), // Default empty history
1474
1313
  setHistory: vi.fn(),
1475
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1476
1314
  };
1477
1315
  client['chat'] = mockChat;
1478
- const mockGenerator = {
1479
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1480
- };
1481
- client['contentGenerator'] = mockGenerator;
1482
1316
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1483
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1317
+ vi.mocked(ideContextStore.get).mockReturnValue({
1484
1318
  workspaceState: {
1485
1319
  openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
1486
1320
  },
@@ -1556,7 +1390,7 @@ ${JSON.stringify({
1556
1390
  openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
1557
1391
  },
1558
1392
  };
1559
- vi.mocked(ideContext.getIdeContext).mockReturnValue(initialIdeContext);
1393
+ vi.mocked(ideContextStore.get).mockReturnValue(initialIdeContext);
1560
1394
  // Act: Send the tool response
1561
1395
  let stream = client.sendMessageStream([
1562
1396
  {
@@ -1602,7 +1436,7 @@ ${JSON.stringify({
1602
1436
  openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
1603
1437
  },
1604
1438
  };
1605
- vi.mocked(ideContext.getIdeContext).mockReturnValue(newIdeContext);
1439
+ vi.mocked(ideContextStore.get).mockReturnValue(newIdeContext);
1606
1440
  // Act: Send a new, regular user message
1607
1441
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1608
1442
  for await (const _ of stream) {
@@ -1632,7 +1466,7 @@ ${JSON.stringify({
1632
1466
  ],
1633
1467
  },
1634
1468
  };
1635
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextA);
1469
+ vi.mocked(ideContextStore.get).mockReturnValue(contextA);
1636
1470
  // Act: Send a regular message to establish the initial context
1637
1471
  let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
1638
1472
  for await (const _ of stream) {
@@ -1665,7 +1499,7 @@ ${JSON.stringify({
1665
1499
  ],
1666
1500
  },
1667
1501
  };
1668
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextB);
1502
+ vi.mocked(ideContextStore.get).mockReturnValue(contextB);
1669
1503
  // Act: Send the tool response
1670
1504
  stream = client.sendMessageStream([
1671
1505
  {
@@ -1710,7 +1544,7 @@ ${JSON.stringify({
1710
1544
  ],
1711
1545
  },
1712
1546
  };
1713
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextC);
1547
+ vi.mocked(ideContextStore.get).mockReturnValue(contextC);
1714
1548
  // Act: Send a new, regular user message
1715
1549
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1716
1550
  for await (const _ of stream) {
@@ -1742,11 +1576,6 @@ ${JSON.stringify({
1742
1576
  getHistory: vi.fn().mockReturnValue([]),
1743
1577
  };
1744
1578
  client['chat'] = mockChat;
1745
- const mockGenerator = {
1746
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1747
- generateContent: mockGenerateContentFn,
1748
- };
1749
- client['contentGenerator'] = mockGenerator;
1750
1579
  // Act
1751
1580
  const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1752
1581
  for await (const _ of stream) {
@@ -1772,11 +1601,6 @@ ${JSON.stringify({
1772
1601
  getHistory: vi.fn().mockReturnValue([]),
1773
1602
  };
1774
1603
  client['chat'] = mockChat;
1775
- const mockGenerator = {
1776
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1777
- generateContent: mockGenerateContentFn,
1778
- };
1779
- client['contentGenerator'] = mockGenerator;
1780
1604
  // Act
1781
1605
  const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1782
1606
  for await (const _ of stream) {
@@ -1785,20 +1609,43 @@ ${JSON.stringify({
1785
1609
  // Assert
1786
1610
  expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
1787
1611
  });
1612
+ it('should abort linked signal when loop is detected', async () => {
1613
+ // Arrange
1614
+ vi.spyOn(client['loopDetector'], 'turnStarted').mockResolvedValue(false);
1615
+ vi.spyOn(client['loopDetector'], 'addAndCheck')
1616
+ .mockReturnValueOnce(false)
1617
+ .mockReturnValueOnce(true);
1618
+ let capturedSignal;
1619
+ mockTurnRunFn.mockImplementation((model, request, signal) => {
1620
+ capturedSignal = signal;
1621
+ return (async function* () {
1622
+ yield { type: 'content', value: 'First event' };
1623
+ yield { type: 'content', value: 'Second event' };
1624
+ })();
1625
+ });
1626
+ const mockChat = {
1627
+ addHistory: vi.fn(),
1628
+ getHistory: vi.fn().mockReturnValue([]),
1629
+ };
1630
+ client['chat'] = mockChat;
1631
+ // Act
1632
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop');
1633
+ const events = [];
1634
+ for await (const event of stream) {
1635
+ events.push(event);
1636
+ }
1637
+ // Assert
1638
+ expect(events).toContainEqual({ type: GeminiEventType.LoopDetected });
1639
+ expect(capturedSignal.aborted).toBe(true);
1640
+ });
1788
1641
  });
1789
1642
  describe('generateContent', () => {
1790
1643
  it('should call generateContent with the correct parameters', async () => {
1791
1644
  const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
1792
1645
  const generationConfig = { temperature: 0.5 };
1793
1646
  const abortSignal = new AbortController().signal;
1794
- // Mock countTokens
1795
- const mockGenerator = {
1796
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
1797
- generateContent: mockGenerateContentFn,
1798
- };
1799
- client['contentGenerator'] = mockGenerator;
1800
1647
  await client.generateContent(contents, generationConfig, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
1801
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
1648
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
1802
1649
  model: DEFAULT_GEMINI_FLASH_MODEL,
1803
1650
  config: {
1804
1651
  abortSignal,
@@ -1814,98 +1661,29 @@ ${JSON.stringify({
1814
1661
  const contents = [{ role: 'user', parts: [{ text: 'test' }] }];
1815
1662
  const currentModel = initialModel + '-changed';
1816
1663
  vi.spyOn(client['config'], 'getModel').mockReturnValueOnce(currentModel);
1817
- const mockGenerator = {
1818
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
1819
- generateContent: mockGenerateContentFn,
1820
- };
1821
- client['contentGenerator'] = mockGenerator;
1822
1664
  await client.generateContent(contents, {}, new AbortController().signal, DEFAULT_GEMINI_FLASH_MODEL);
1823
- expect(mockGenerateContentFn).not.toHaveBeenCalledWith({
1665
+ expect(mockContentGenerator.generateContent).not.toHaveBeenCalledWith({
1824
1666
  model: initialModel,
1825
1667
  config: expect.any(Object),
1826
1668
  contents,
1827
1669
  });
1828
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
1670
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
1829
1671
  model: DEFAULT_GEMINI_FLASH_MODEL,
1830
1672
  config: expect.any(Object),
1831
1673
  contents,
1832
1674
  }, 'test-session-id');
1833
1675
  });
1834
- });
1835
- describe('handleFlashFallback', () => {
1836
- it('should use current model from config when checking for fallback', async () => {
1837
- const initialModel = client['config'].getModel();
1838
- const fallbackModel = DEFAULT_GEMINI_FLASH_MODEL;
1839
- // mock config been changed
1840
- const currentModel = initialModel + '-changed';
1841
- const getModelSpy = vi.spyOn(client['config'], 'getModel');
1842
- getModelSpy.mockReturnValue(currentModel);
1843
- const mockFallbackHandler = vi.fn().mockResolvedValue(true);
1844
- client['config'].flashFallbackHandler = mockFallbackHandler;
1845
- client['config'].setModel = vi.fn();
1846
- const result = await client['handleFlashFallback'](AuthType.LOGIN_WITH_GOOGLE);
1847
- expect(result).toBe(fallbackModel);
1848
- expect(mockFallbackHandler).toHaveBeenCalledWith(currentModel, fallbackModel, undefined);
1849
- });
1850
- });
1851
- describe('setHistory', () => {
1852
- it('should strip thought signatures when stripThoughts is true', () => {
1853
- const mockChat = {
1854
- setHistory: vi.fn(),
1855
- };
1856
- client['chat'] = mockChat;
1857
- const historyWithThoughts = [
1858
- {
1859
- role: 'user',
1860
- parts: [{ text: 'hello' }],
1861
- },
1862
- {
1863
- role: 'model',
1864
- parts: [
1865
- { text: 'thinking...', thoughtSignature: 'thought-123' },
1866
- {
1867
- functionCall: { name: 'test', args: {} },
1868
- thoughtSignature: 'thought-456',
1869
- },
1870
- ],
1871
- },
1872
- ];
1873
- client.setHistory(historyWithThoughts, { stripThoughts: true });
1874
- const expectedHistory = [
1875
- {
1876
- role: 'user',
1877
- parts: [{ text: 'hello' }],
1878
- },
1879
- {
1880
- role: 'model',
1881
- parts: [
1882
- { text: 'thinking...' },
1883
- { functionCall: { name: 'test', args: {} } },
1884
- ],
1885
- },
1886
- ];
1887
- expect(mockChat.setHistory).toHaveBeenCalledWith(expectedHistory);
1888
- });
1889
- it('should not strip thought signatures when stripThoughts is false', () => {
1890
- const mockChat = {
1891
- setHistory: vi.fn(),
1892
- };
1893
- client['chat'] = mockChat;
1894
- const historyWithThoughts = [
1895
- {
1896
- role: 'user',
1897
- parts: [{ text: 'hello' }],
1898
- },
1899
- {
1900
- role: 'model',
1901
- parts: [
1902
- { text: 'thinking...', thoughtSignature: 'thought-123' },
1903
- { text: 'ok', thoughtSignature: 'thought-456' },
1904
- ],
1905
- },
1906
- ];
1907
- client.setHistory(historyWithThoughts, { stripThoughts: false });
1908
- expect(mockChat.setHistory).toHaveBeenCalledWith(historyWithThoughts);
1676
+ it('should use the Flash model when fallback mode is active', async () => {
1677
+ const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
1678
+ const generationConfig = { temperature: 0.5 };
1679
+ const abortSignal = new AbortController().signal;
1680
+ const requestedModel = 'gemini-2.5-pro'; // A non-flash model
1681
+ // Mock config to be in fallback mode
1682
+ vi.spyOn(client['config'], 'isInFallbackMode').mockReturnValue(true);
1683
+ await client.generateContent(contents, generationConfig, abortSignal, requestedModel);
1684
+ expect(mockGenerateContentFn).toHaveBeenCalledWith(expect.objectContaining({
1685
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1686
+ }), 'test-session-id');
1909
1687
  });
1910
1688
  });
1911
1689
  });