@google/gemini-cli-core 0.0.1

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 (854) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +320 -0
  3. package/dist/.last_build +0 -0
  4. package/dist/index.d.ts +16 -0
  5. package/dist/index.js +17 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/__mocks__/fs/promises.d.ts +11 -0
  8. package/dist/src/__mocks__/fs/promises.js +17 -0
  9. package/dist/src/__mocks__/fs/promises.js.map +1 -0
  10. package/dist/src/code_assist/codeAssist.d.ts +12 -0
  11. package/dist/src/code_assist/codeAssist.js +31 -0
  12. package/dist/src/code_assist/codeAssist.js.map +1 -0
  13. package/dist/src/code_assist/converter.d.ts +74 -0
  14. package/dist/src/code_assist/converter.js +160 -0
  15. package/dist/src/code_assist/converter.js.map +1 -0
  16. package/dist/src/code_assist/converter.test.d.ts +6 -0
  17. package/dist/src/code_assist/converter.test.js +372 -0
  18. package/dist/src/code_assist/converter.test.js.map +1 -0
  19. package/dist/src/code_assist/oauth-credential-storage.d.ts +25 -0
  20. package/dist/src/code_assist/oauth-credential-storage.js +109 -0
  21. package/dist/src/code_assist/oauth-credential-storage.js.map +1 -0
  22. package/dist/src/code_assist/oauth-credential-storage.test.d.ts +6 -0
  23. package/dist/src/code_assist/oauth-credential-storage.test.js +136 -0
  24. package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -0
  25. package/dist/src/code_assist/oauth2.d.ts +22 -0
  26. package/dist/src/code_assist/oauth2.js +416 -0
  27. package/dist/src/code_assist/oauth2.js.map +1 -0
  28. package/dist/src/code_assist/oauth2.test.d.ts +6 -0
  29. package/dist/src/code_assist/oauth2.test.js +817 -0
  30. package/dist/src/code_assist/oauth2.test.js.map +1 -0
  31. package/dist/src/code_assist/server.d.ts +37 -0
  32. package/dist/src/code_assist/server.js +148 -0
  33. package/dist/src/code_assist/server.js.map +1 -0
  34. package/dist/src/code_assist/server.test.d.ts +6 -0
  35. package/dist/src/code_assist/server.test.js +159 -0
  36. package/dist/src/code_assist/server.test.js.map +1 -0
  37. package/dist/src/code_assist/setup.d.ts +20 -0
  38. package/dist/src/code_assist/setup.js +101 -0
  39. package/dist/src/code_assist/setup.js.map +1 -0
  40. package/dist/src/code_assist/setup.test.d.ts +6 -0
  41. package/dist/src/code_assist/setup.test.js +171 -0
  42. package/dist/src/code_assist/setup.test.js.map +1 -0
  43. package/dist/src/code_assist/types.d.ts +163 -0
  44. package/dist/src/code_assist/types.js +46 -0
  45. package/dist/src/code_assist/types.js.map +1 -0
  46. package/dist/src/config/config.d.ts +378 -0
  47. package/dist/src/config/config.js +765 -0
  48. package/dist/src/config/config.js.map +1 -0
  49. package/dist/src/config/config.test.d.ts +6 -0
  50. package/dist/src/config/config.test.js +712 -0
  51. package/dist/src/config/config.test.js.map +1 -0
  52. package/dist/src/config/flashFallback.test.d.ts +6 -0
  53. package/dist/src/config/flashFallback.test.js +87 -0
  54. package/dist/src/config/flashFallback.test.js.map +1 -0
  55. package/dist/src/config/models.d.ts +25 -0
  56. package/dist/src/config/models.js +39 -0
  57. package/dist/src/config/models.js.map +1 -0
  58. package/dist/src/config/models.test.d.ts +6 -0
  59. package/dist/src/config/models.test.js +55 -0
  60. package/dist/src/config/models.test.js.map +1 -0
  61. package/dist/src/config/storage.d.ts +34 -0
  62. package/dist/src/config/storage.js +95 -0
  63. package/dist/src/config/storage.js.map +1 -0
  64. package/dist/src/config/storage.test.d.ts +6 -0
  65. package/dist/src/config/storage.test.js +47 -0
  66. package/dist/src/config/storage.test.js.map +1 -0
  67. package/dist/src/confirmation-bus/index.d.ts +7 -0
  68. package/dist/src/confirmation-bus/index.js +8 -0
  69. package/dist/src/confirmation-bus/index.js.map +1 -0
  70. package/dist/src/confirmation-bus/message-bus.d.ts +17 -0
  71. package/dist/src/confirmation-bus/message-bus.js +81 -0
  72. package/dist/src/confirmation-bus/message-bus.js.map +1 -0
  73. package/dist/src/confirmation-bus/message-bus.test.d.ts +6 -0
  74. package/dist/src/confirmation-bus/message-bus.test.js +164 -0
  75. package/dist/src/confirmation-bus/message-bus.test.js.map +1 -0
  76. package/dist/src/confirmation-bus/types.d.ts +38 -0
  77. package/dist/src/confirmation-bus/types.js +15 -0
  78. package/dist/src/confirmation-bus/types.js.map +1 -0
  79. package/dist/src/core/baseLlmClient.d.ts +46 -0
  80. package/dist/src/core/baseLlmClient.js +112 -0
  81. package/dist/src/core/baseLlmClient.js.map +1 -0
  82. package/dist/src/core/baseLlmClient.test.d.ts +6 -0
  83. package/dist/src/core/baseLlmClient.test.js +253 -0
  84. package/dist/src/core/baseLlmClient.test.js.map +1 -0
  85. package/dist/src/core/client.d.ts +60 -0
  86. package/dist/src/core/client.js +602 -0
  87. package/dist/src/core/client.js.map +1 -0
  88. package/dist/src/core/client.test.d.ts +6 -0
  89. package/dist/src/core/client.test.js +1758 -0
  90. package/dist/src/core/client.test.js.map +1 -0
  91. package/dist/src/core/contentGenerator.d.ts +32 -0
  92. package/dist/src/core/contentGenerator.js +76 -0
  93. package/dist/src/core/contentGenerator.js.map +1 -0
  94. package/dist/src/core/contentGenerator.test.d.ts +6 -0
  95. package/dist/src/core/contentGenerator.test.js +122 -0
  96. package/dist/src/core/contentGenerator.test.js.map +1 -0
  97. package/dist/src/core/coreToolScheduler.d.ts +131 -0
  98. package/dist/src/core/coreToolScheduler.js +706 -0
  99. package/dist/src/core/coreToolScheduler.js.map +1 -0
  100. package/dist/src/core/coreToolScheduler.test.d.ts +6 -0
  101. package/dist/src/core/coreToolScheduler.test.js +1151 -0
  102. package/dist/src/core/coreToolScheduler.test.js.map +1 -0
  103. package/dist/src/core/geminiChat.d.ts +128 -0
  104. package/dist/src/core/geminiChat.js +537 -0
  105. package/dist/src/core/geminiChat.js.map +1 -0
  106. package/dist/src/core/geminiChat.test.d.ts +6 -0
  107. package/dist/src/core/geminiChat.test.js +1163 -0
  108. package/dist/src/core/geminiChat.test.js.map +1 -0
  109. package/dist/src/core/geminiRequest.d.ts +13 -0
  110. package/dist/src/core/geminiRequest.js +11 -0
  111. package/dist/src/core/geminiRequest.js.map +1 -0
  112. package/dist/src/core/logger.d.ts +60 -0
  113. package/dist/src/core/logger.js +360 -0
  114. package/dist/src/core/logger.js.map +1 -0
  115. package/dist/src/core/logger.test.d.ts +6 -0
  116. package/dist/src/core/logger.test.js +534 -0
  117. package/dist/src/core/logger.test.js.map +1 -0
  118. package/dist/src/core/loggingContentGenerator.d.ts +25 -0
  119. package/dist/src/core/loggingContentGenerator.js +94 -0
  120. package/dist/src/core/loggingContentGenerator.js.map +1 -0
  121. package/dist/src/core/nonInteractiveToolExecutor.d.ts +10 -0
  122. package/dist/src/core/nonInteractiveToolExecutor.js +24 -0
  123. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -0
  124. package/dist/src/core/nonInteractiveToolExecutor.test.d.ts +6 -0
  125. package/dist/src/core/nonInteractiveToolExecutor.test.js +294 -0
  126. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -0
  127. package/dist/src/core/prompts.d.ts +17 -0
  128. package/dist/src/core/prompts.js +380 -0
  129. package/dist/src/core/prompts.js.map +1 -0
  130. package/dist/src/core/prompts.test.d.ts +6 -0
  131. package/dist/src/core/prompts.test.js +343 -0
  132. package/dist/src/core/prompts.test.js.map +1 -0
  133. package/dist/src/core/subagent.d.ts +236 -0
  134. package/dist/src/core/subagent.js +482 -0
  135. package/dist/src/core/subagent.js.map +1 -0
  136. package/dist/src/core/subagent.test.d.ts +6 -0
  137. package/dist/src/core/subagent.test.js +530 -0
  138. package/dist/src/core/subagent.test.js.map +1 -0
  139. package/dist/src/core/tokenLimits.d.ts +10 -0
  140. package/dist/src/core/tokenLimits.js +28 -0
  141. package/dist/src/core/tokenLimits.js.map +1 -0
  142. package/dist/src/core/turn.d.ts +141 -0
  143. package/dist/src/core/turn.js +188 -0
  144. package/dist/src/core/turn.js.map +1 -0
  145. package/dist/src/core/turn.test.d.ts +6 -0
  146. package/dist/src/core/turn.test.js +628 -0
  147. package/dist/src/core/turn.test.js.map +1 -0
  148. package/dist/src/fallback/handler.d.ts +7 -0
  149. package/dist/src/fallback/handler.js +51 -0
  150. package/dist/src/fallback/handler.js.map +1 -0
  151. package/dist/src/fallback/handler.test.d.ts +6 -0
  152. package/dist/src/fallback/handler.test.js +130 -0
  153. package/dist/src/fallback/handler.test.js.map +1 -0
  154. package/dist/src/fallback/types.d.ts +14 -0
  155. package/dist/src/fallback/types.js +7 -0
  156. package/dist/src/fallback/types.js.map +1 -0
  157. package/dist/src/generated/git-commit.d.ts +7 -0
  158. package/dist/src/generated/git-commit.js +10 -0
  159. package/dist/src/generated/git-commit.js.map +1 -0
  160. package/dist/src/ide/constants.d.ts +9 -0
  161. package/dist/src/ide/constants.js +10 -0
  162. package/dist/src/ide/constants.js.map +1 -0
  163. package/dist/src/ide/detect-ide.d.ts +53 -0
  164. package/dist/src/ide/detect-ide.js +58 -0
  165. package/dist/src/ide/detect-ide.js.map +1 -0
  166. package/dist/src/ide/detect-ide.test.d.ts +6 -0
  167. package/dist/src/ide/detect-ide.test.js +69 -0
  168. package/dist/src/ide/detect-ide.test.js.map +1 -0
  169. package/dist/src/ide/ide-client.d.ts +109 -0
  170. package/dist/src/ide/ide-client.js +640 -0
  171. package/dist/src/ide/ide-client.js.map +1 -0
  172. package/dist/src/ide/ide-client.test.d.ts +6 -0
  173. package/dist/src/ide/ide-client.test.js +388 -0
  174. package/dist/src/ide/ide-client.test.js.map +1 -0
  175. package/dist/src/ide/ide-installer.d.ts +14 -0
  176. package/dist/src/ide/ide-installer.js +111 -0
  177. package/dist/src/ide/ide-installer.js.map +1 -0
  178. package/dist/src/ide/ide-installer.test.d.ts +6 -0
  179. package/dist/src/ide/ide-installer.test.js +131 -0
  180. package/dist/src/ide/ide-installer.test.js.map +1 -0
  181. package/dist/src/ide/ideContext.d.ts +44 -0
  182. package/dist/src/ide/ideContext.js +101 -0
  183. package/dist/src/ide/ideContext.js.map +1 -0
  184. package/dist/src/ide/ideContext.test.d.ts +6 -0
  185. package/dist/src/ide/ideContext.test.js +393 -0
  186. package/dist/src/ide/ideContext.test.js.map +1 -0
  187. package/dist/src/ide/process-utils.d.ts +21 -0
  188. package/dist/src/ide/process-utils.js +171 -0
  189. package/dist/src/ide/process-utils.js.map +1 -0
  190. package/dist/src/ide/process-utils.test.d.ts +6 -0
  191. package/dist/src/ide/process-utils.test.js +158 -0
  192. package/dist/src/ide/process-utils.test.js.map +1 -0
  193. package/dist/src/ide/types.d.ts +486 -0
  194. package/dist/src/ide/types.js +138 -0
  195. package/dist/src/ide/types.js.map +1 -0
  196. package/dist/src/index.d.ts +87 -0
  197. package/dist/src/index.js +97 -0
  198. package/dist/src/index.js.map +1 -0
  199. package/dist/src/index.test.d.ts +6 -0
  200. package/dist/src/index.test.js +12 -0
  201. package/dist/src/index.test.js.map +1 -0
  202. package/dist/src/mcp/google-auth-provider.d.ts +23 -0
  203. package/dist/src/mcp/google-auth-provider.js +72 -0
  204. package/dist/src/mcp/google-auth-provider.js.map +1 -0
  205. package/dist/src/mcp/google-auth-provider.test.d.ts +6 -0
  206. package/dist/src/mcp/google-auth-provider.test.js +89 -0
  207. package/dist/src/mcp/google-auth-provider.test.js.map +1 -0
  208. package/dist/src/mcp/oauth-provider.d.ts +149 -0
  209. package/dist/src/mcp/oauth-provider.js +608 -0
  210. package/dist/src/mcp/oauth-provider.js.map +1 -0
  211. package/dist/src/mcp/oauth-provider.test.d.ts +6 -0
  212. package/dist/src/mcp/oauth-provider.test.js +711 -0
  213. package/dist/src/mcp/oauth-provider.test.js.map +1 -0
  214. package/dist/src/mcp/oauth-token-storage.d.ts +65 -0
  215. package/dist/src/mcp/oauth-token-storage.js +180 -0
  216. package/dist/src/mcp/oauth-token-storage.js.map +1 -0
  217. package/dist/src/mcp/oauth-token-storage.test.d.ts +6 -0
  218. package/dist/src/mcp/oauth-token-storage.test.js +299 -0
  219. package/dist/src/mcp/oauth-token-storage.test.js.map +1 -0
  220. package/dist/src/mcp/oauth-utils.d.ts +119 -0
  221. package/dist/src/mcp/oauth-utils.js +235 -0
  222. package/dist/src/mcp/oauth-utils.js.map +1 -0
  223. package/dist/src/mcp/oauth-utils.test.d.ts +6 -0
  224. package/dist/src/mcp/oauth-utils.test.js +199 -0
  225. package/dist/src/mcp/oauth-utils.test.js.map +1 -0
  226. package/dist/src/mcp/token-storage/base-token-storage.d.ts +19 -0
  227. package/dist/src/mcp/token-storage/base-token-storage.js +36 -0
  228. package/dist/src/mcp/token-storage/base-token-storage.js.map +1 -0
  229. package/dist/src/mcp/token-storage/base-token-storage.test.d.ts +6 -0
  230. package/dist/src/mcp/token-storage/base-token-storage.test.js +160 -0
  231. package/dist/src/mcp/token-storage/base-token-storage.test.js.map +1 -0
  232. package/dist/src/mcp/token-storage/file-token-storage.d.ts +24 -0
  233. package/dist/src/mcp/token-storage/file-token-storage.js +144 -0
  234. package/dist/src/mcp/token-storage/file-token-storage.js.map +1 -0
  235. package/dist/src/mcp/token-storage/file-token-storage.test.d.ts +6 -0
  236. package/dist/src/mcp/token-storage/file-token-storage.test.js +235 -0
  237. package/dist/src/mcp/token-storage/file-token-storage.test.js.map +1 -0
  238. package/dist/src/mcp/token-storage/hybrid-token-storage.d.ts +23 -0
  239. package/dist/src/mcp/token-storage/hybrid-token-storage.js +78 -0
  240. package/dist/src/mcp/token-storage/hybrid-token-storage.js.map +1 -0
  241. package/dist/src/mcp/token-storage/hybrid-token-storage.test.d.ts +6 -0
  242. package/dist/src/mcp/token-storage/hybrid-token-storage.test.js +193 -0
  243. package/dist/src/mcp/token-storage/hybrid-token-storage.test.js.map +1 -0
  244. package/dist/src/mcp/token-storage/index.d.ts +11 -0
  245. package/dist/src/mcp/token-storage/index.js +12 -0
  246. package/dist/src/mcp/token-storage/index.js.map +1 -0
  247. package/dist/src/mcp/token-storage/keychain-token-storage.d.ts +31 -0
  248. package/dist/src/mcp/token-storage/keychain-token-storage.js +190 -0
  249. package/dist/src/mcp/token-storage/keychain-token-storage.js.map +1 -0
  250. package/dist/src/mcp/token-storage/keychain-token-storage.test.d.ts +6 -0
  251. package/dist/src/mcp/token-storage/keychain-token-storage.test.js +254 -0
  252. package/dist/src/mcp/token-storage/keychain-token-storage.test.js.map +1 -0
  253. package/dist/src/mcp/token-storage/types.d.ts +38 -0
  254. package/dist/src/mcp/token-storage/types.js +11 -0
  255. package/dist/src/mcp/token-storage/types.js.map +1 -0
  256. package/dist/src/mocks/msw.d.ts +6 -0
  257. package/dist/src/mocks/msw.js +8 -0
  258. package/dist/src/mocks/msw.js.map +1 -0
  259. package/dist/src/output/json-formatter.d.ts +11 -0
  260. package/dist/src/output/json-formatter.js +30 -0
  261. package/dist/src/output/json-formatter.js.map +1 -0
  262. package/dist/src/output/json-formatter.test.d.ts +6 -0
  263. package/dist/src/output/json-formatter.test.js +266 -0
  264. package/dist/src/output/json-formatter.test.js.map +1 -0
  265. package/dist/src/output/types.d.ts +20 -0
  266. package/dist/src/output/types.js +11 -0
  267. package/dist/src/output/types.js.map +1 -0
  268. package/dist/src/policy/index.d.ts +7 -0
  269. package/dist/src/policy/index.js +8 -0
  270. package/dist/src/policy/index.js.map +1 -0
  271. package/dist/src/policy/policy-engine.d.ts +30 -0
  272. package/dist/src/policy/policy-engine.js +83 -0
  273. package/dist/src/policy/policy-engine.js.map +1 -0
  274. package/dist/src/policy/policy-engine.test.d.ts +6 -0
  275. package/dist/src/policy/policy-engine.test.js +470 -0
  276. package/dist/src/policy/policy-engine.test.js.map +1 -0
  277. package/dist/src/policy/stable-stringify.d.ts +58 -0
  278. package/dist/src/policy/stable-stringify.js +122 -0
  279. package/dist/src/policy/stable-stringify.js.map +1 -0
  280. package/dist/src/policy/types.d.ts +47 -0
  281. package/dist/src/policy/types.js +12 -0
  282. package/dist/src/policy/types.js.map +1 -0
  283. package/dist/src/prompts/mcp-prompts.d.ts +8 -0
  284. package/dist/src/prompts/mcp-prompts.js +13 -0
  285. package/dist/src/prompts/mcp-prompts.js.map +1 -0
  286. package/dist/src/prompts/prompt-registry.d.ts +34 -0
  287. package/dist/src/prompts/prompt-registry.js +63 -0
  288. package/dist/src/prompts/prompt-registry.js.map +1 -0
  289. package/dist/src/routing/modelRouterService.d.ts +23 -0
  290. package/dist/src/routing/modelRouterService.js +70 -0
  291. package/dist/src/routing/modelRouterService.js.map +1 -0
  292. package/dist/src/routing/modelRouterService.test.d.ts +6 -0
  293. package/dist/src/routing/modelRouterService.test.js +98 -0
  294. package/dist/src/routing/modelRouterService.test.js.map +1 -0
  295. package/dist/src/routing/routingStrategy.d.ts +62 -0
  296. package/dist/src/routing/routingStrategy.js +7 -0
  297. package/dist/src/routing/routingStrategy.js.map +1 -0
  298. package/dist/src/routing/strategies/classifierStrategy.d.ts +12 -0
  299. package/dist/src/routing/strategies/classifierStrategy.js +173 -0
  300. package/dist/src/routing/strategies/classifierStrategy.js.map +1 -0
  301. package/dist/src/routing/strategies/classifierStrategy.test.d.ts +6 -0
  302. package/dist/src/routing/strategies/classifierStrategy.test.js +192 -0
  303. package/dist/src/routing/strategies/classifierStrategy.test.js.map +1 -0
  304. package/dist/src/routing/strategies/compositeStrategy.d.ts +26 -0
  305. package/dist/src/routing/strategies/compositeStrategy.js +67 -0
  306. package/dist/src/routing/strategies/compositeStrategy.js.map +1 -0
  307. package/dist/src/routing/strategies/compositeStrategy.test.d.ts +6 -0
  308. package/dist/src/routing/strategies/compositeStrategy.test.js +123 -0
  309. package/dist/src/routing/strategies/compositeStrategy.test.js.map +1 -0
  310. package/dist/src/routing/strategies/defaultStrategy.d.ts +12 -0
  311. package/dist/src/routing/strategies/defaultStrategy.js +20 -0
  312. package/dist/src/routing/strategies/defaultStrategy.js.map +1 -0
  313. package/dist/src/routing/strategies/defaultStrategy.test.d.ts +6 -0
  314. package/dist/src/routing/strategies/defaultStrategy.test.js +26 -0
  315. package/dist/src/routing/strategies/defaultStrategy.test.js.map +1 -0
  316. package/dist/src/routing/strategies/fallbackStrategy.d.ts +12 -0
  317. package/dist/src/routing/strategies/fallbackStrategy.js +25 -0
  318. package/dist/src/routing/strategies/fallbackStrategy.js.map +1 -0
  319. package/dist/src/routing/strategies/fallbackStrategy.test.d.ts +6 -0
  320. package/dist/src/routing/strategies/fallbackStrategy.test.js +55 -0
  321. package/dist/src/routing/strategies/fallbackStrategy.test.js.map +1 -0
  322. package/dist/src/routing/strategies/overrideStrategy.d.ts +15 -0
  323. package/dist/src/routing/strategies/overrideStrategy.js +28 -0
  324. package/dist/src/routing/strategies/overrideStrategy.js.map +1 -0
  325. package/dist/src/routing/strategies/overrideStrategy.test.d.ts +6 -0
  326. package/dist/src/routing/strategies/overrideStrategy.test.js +42 -0
  327. package/dist/src/routing/strategies/overrideStrategy.test.js.map +1 -0
  328. package/dist/src/services/chatRecordingService.d.ts +144 -0
  329. package/dist/src/services/chatRecordingService.js +330 -0
  330. package/dist/src/services/chatRecordingService.js.map +1 -0
  331. package/dist/src/services/chatRecordingService.test.d.ts +6 -0
  332. package/dist/src/services/chatRecordingService.test.js +332 -0
  333. package/dist/src/services/chatRecordingService.test.js.map +1 -0
  334. package/dist/src/services/fileDiscoveryService.d.ts +45 -0
  335. package/dist/src/services/fileDiscoveryService.js +105 -0
  336. package/dist/src/services/fileDiscoveryService.js.map +1 -0
  337. package/dist/src/services/fileDiscoveryService.test.d.ts +6 -0
  338. package/dist/src/services/fileDiscoveryService.test.js +143 -0
  339. package/dist/src/services/fileDiscoveryService.test.js.map +1 -0
  340. package/dist/src/services/fileSystemService.d.ts +31 -0
  341. package/dist/src/services/fileSystemService.js +18 -0
  342. package/dist/src/services/fileSystemService.js.map +1 -0
  343. package/dist/src/services/fileSystemService.test.d.ts +6 -0
  344. package/dist/src/services/fileSystemService.test.js +41 -0
  345. package/dist/src/services/fileSystemService.test.js.map +1 -0
  346. package/dist/src/services/gitService.d.ts +23 -0
  347. package/dist/src/services/gitService.js +107 -0
  348. package/dist/src/services/gitService.js.map +1 -0
  349. package/dist/src/services/gitService.test.d.ts +6 -0
  350. package/dist/src/services/gitService.test.js +202 -0
  351. package/dist/src/services/gitService.test.js.map +1 -0
  352. package/dist/src/services/loopDetectionService.d.ts +103 -0
  353. package/dist/src/services/loopDetectionService.js +379 -0
  354. package/dist/src/services/loopDetectionService.js.map +1 -0
  355. package/dist/src/services/loopDetectionService.test.d.ts +6 -0
  356. package/dist/src/services/loopDetectionService.test.js +587 -0
  357. package/dist/src/services/loopDetectionService.test.js.map +1 -0
  358. package/dist/src/services/shellExecutionService.d.ts +100 -0
  359. package/dist/src/services/shellExecutionService.js +466 -0
  360. package/dist/src/services/shellExecutionService.js.map +1 -0
  361. package/dist/src/services/shellExecutionService.test.d.ts +6 -0
  362. package/dist/src/services/shellExecutionService.test.js +614 -0
  363. package/dist/src/services/shellExecutionService.test.js.map +1 -0
  364. package/dist/src/telemetry/activity-detector.d.ts +41 -0
  365. package/dist/src/telemetry/activity-detector.js +61 -0
  366. package/dist/src/telemetry/activity-detector.js.map +1 -0
  367. package/dist/src/telemetry/activity-detector.test.d.ts +6 -0
  368. package/dist/src/telemetry/activity-detector.test.js +136 -0
  369. package/dist/src/telemetry/activity-detector.test.js.map +1 -0
  370. package/dist/src/telemetry/activity-types.d.ts +19 -0
  371. package/dist/src/telemetry/activity-types.js +21 -0
  372. package/dist/src/telemetry/activity-types.js.map +1 -0
  373. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +135 -0
  374. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +892 -0
  375. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -0
  376. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.d.ts +17 -0
  377. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +507 -0
  378. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -0
  379. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +107 -0
  380. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +275 -0
  381. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -0
  382. package/dist/src/telemetry/constants.d.ts +40 -0
  383. package/dist/src/telemetry/constants.js +41 -0
  384. package/dist/src/telemetry/constants.js.map +1 -0
  385. package/dist/src/telemetry/file-exporters.d.ts +29 -0
  386. package/dist/src/telemetry/file-exporters.js +62 -0
  387. package/dist/src/telemetry/file-exporters.js.map +1 -0
  388. package/dist/src/telemetry/gcp-exporters.d.ts +34 -0
  389. package/dist/src/telemetry/gcp-exporters.js +117 -0
  390. package/dist/src/telemetry/gcp-exporters.js.map +1 -0
  391. package/dist/src/telemetry/gcp-exporters.test.d.ts +6 -0
  392. package/dist/src/telemetry/gcp-exporters.test.js +318 -0
  393. package/dist/src/telemetry/gcp-exporters.test.js.map +1 -0
  394. package/dist/src/telemetry/high-water-mark-tracker.d.ts +43 -0
  395. package/dist/src/telemetry/high-water-mark-tracker.js +88 -0
  396. package/dist/src/telemetry/high-water-mark-tracker.js.map +1 -0
  397. package/dist/src/telemetry/high-water-mark-tracker.test.d.ts +6 -0
  398. package/dist/src/telemetry/high-water-mark-tracker.test.js +152 -0
  399. package/dist/src/telemetry/high-water-mark-tracker.test.js.map +1 -0
  400. package/dist/src/telemetry/index.d.ts +26 -0
  401. package/dist/src/telemetry/index.js +26 -0
  402. package/dist/src/telemetry/index.js.map +1 -0
  403. package/dist/src/telemetry/integration.test.circular.d.ts +6 -0
  404. package/dist/src/telemetry/integration.test.circular.js +54 -0
  405. package/dist/src/telemetry/integration.test.circular.js.map +1 -0
  406. package/dist/src/telemetry/loggers.d.ts +33 -0
  407. package/dist/src/telemetry/loggers.js +536 -0
  408. package/dist/src/telemetry/loggers.js.map +1 -0
  409. package/dist/src/telemetry/loggers.test.circular.d.ts +6 -0
  410. package/dist/src/telemetry/loggers.test.circular.js +107 -0
  411. package/dist/src/telemetry/loggers.test.circular.js.map +1 -0
  412. package/dist/src/telemetry/loggers.test.d.ts +6 -0
  413. package/dist/src/telemetry/loggers.test.js +887 -0
  414. package/dist/src/telemetry/loggers.test.js.map +1 -0
  415. package/dist/src/telemetry/metrics.d.ts +37 -0
  416. package/dist/src/telemetry/metrics.js +231 -0
  417. package/dist/src/telemetry/metrics.js.map +1 -0
  418. package/dist/src/telemetry/metrics.test.d.ts +6 -0
  419. package/dist/src/telemetry/metrics.test.js +264 -0
  420. package/dist/src/telemetry/metrics.test.js.map +1 -0
  421. package/dist/src/telemetry/rate-limiter.d.ts +48 -0
  422. package/dist/src/telemetry/rate-limiter.js +100 -0
  423. package/dist/src/telemetry/rate-limiter.js.map +1 -0
  424. package/dist/src/telemetry/rate-limiter.test.d.ts +6 -0
  425. package/dist/src/telemetry/rate-limiter.test.js +207 -0
  426. package/dist/src/telemetry/rate-limiter.test.js.map +1 -0
  427. package/dist/src/telemetry/sdk.d.ts +9 -0
  428. package/dist/src/telemetry/sdk.js +178 -0
  429. package/dist/src/telemetry/sdk.js.map +1 -0
  430. package/dist/src/telemetry/sdk.test.d.ts +6 -0
  431. package/dist/src/telemetry/sdk.test.js +177 -0
  432. package/dist/src/telemetry/sdk.test.js.map +1 -0
  433. package/dist/src/telemetry/telemetry-utils.d.ts +6 -0
  434. package/dist/src/telemetry/telemetry-utils.js +14 -0
  435. package/dist/src/telemetry/telemetry-utils.js.map +1 -0
  436. package/dist/src/telemetry/telemetry-utils.test.d.ts +6 -0
  437. package/dist/src/telemetry/telemetry-utils.test.js +40 -0
  438. package/dist/src/telemetry/telemetry-utils.test.js.map +1 -0
  439. package/dist/src/telemetry/telemetry.test.d.ts +6 -0
  440. package/dist/src/telemetry/telemetry.test.js +50 -0
  441. package/dist/src/telemetry/telemetry.test.js.map +1 -0
  442. package/dist/src/telemetry/tool-call-decision.d.ts +13 -0
  443. package/dist/src/telemetry/tool-call-decision.js +29 -0
  444. package/dist/src/telemetry/tool-call-decision.js.map +1 -0
  445. package/dist/src/telemetry/types.d.ts +284 -0
  446. package/dist/src/telemetry/types.js +487 -0
  447. package/dist/src/telemetry/types.js.map +1 -0
  448. package/dist/src/telemetry/uiTelemetry.d.ts +75 -0
  449. package/dist/src/telemetry/uiTelemetry.js +152 -0
  450. package/dist/src/telemetry/uiTelemetry.js.map +1 -0
  451. package/dist/src/telemetry/uiTelemetry.test.d.ts +6 -0
  452. package/dist/src/telemetry/uiTelemetry.test.js +558 -0
  453. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -0
  454. package/dist/src/test-utils/config.d.ts +17 -0
  455. package/dist/src/test-utils/config.js +32 -0
  456. package/dist/src/test-utils/config.js.map +1 -0
  457. package/dist/src/test-utils/index.d.ts +6 -0
  458. package/dist/src/test-utils/index.js +7 -0
  459. package/dist/src/test-utils/index.js.map +1 -0
  460. package/dist/src/test-utils/mock-tool.d.ts +41 -0
  461. package/dist/src/test-utils/mock-tool.js +51 -0
  462. package/dist/src/test-utils/mock-tool.js.map +1 -0
  463. package/dist/src/test-utils/mockWorkspaceContext.d.ts +13 -0
  464. package/dist/src/test-utils/mockWorkspaceContext.js +24 -0
  465. package/dist/src/test-utils/mockWorkspaceContext.js.map +1 -0
  466. package/dist/src/test-utils/tools.d.ts +45 -0
  467. package/dist/src/test-utils/tools.js +105 -0
  468. package/dist/src/test-utils/tools.js.map +1 -0
  469. package/dist/src/tools/diffOptions.d.ts +9 -0
  470. package/dist/src/tools/diffOptions.js +46 -0
  471. package/dist/src/tools/diffOptions.js.map +1 -0
  472. package/dist/src/tools/diffOptions.test.d.ts +6 -0
  473. package/dist/src/tools/diffOptions.test.js +155 -0
  474. package/dist/src/tools/diffOptions.test.js.map +1 -0
  475. package/dist/src/tools/edit.d.ts +56 -0
  476. package/dist/src/tools/edit.js +414 -0
  477. package/dist/src/tools/edit.js.map +1 -0
  478. package/dist/src/tools/edit.test.d.ts +6 -0
  479. package/dist/src/tools/edit.test.js +824 -0
  480. package/dist/src/tools/edit.test.js.map +1 -0
  481. package/dist/src/tools/glob.d.ts +56 -0
  482. package/dist/src/tools/glob.js +243 -0
  483. package/dist/src/tools/glob.js.map +1 -0
  484. package/dist/src/tools/glob.test.d.ts +6 -0
  485. package/dist/src/tools/glob.test.js +426 -0
  486. package/dist/src/tools/glob.test.js.map +1 -0
  487. package/dist/src/tools/grep.d.ts +47 -0
  488. package/dist/src/tools/grep.js +517 -0
  489. package/dist/src/tools/grep.js.map +1 -0
  490. package/dist/src/tools/grep.test.d.ts +6 -0
  491. package/dist/src/tools/grep.test.js +295 -0
  492. package/dist/src/tools/grep.test.js.map +1 -0
  493. package/dist/src/tools/ls.d.ts +68 -0
  494. package/dist/src/tools/ls.js +214 -0
  495. package/dist/src/tools/ls.js.map +1 -0
  496. package/dist/src/tools/ls.test.d.ts +6 -0
  497. package/dist/src/tools/ls.test.js +249 -0
  498. package/dist/src/tools/ls.test.js.map +1 -0
  499. package/dist/src/tools/mcp-client-manager.d.ts +40 -0
  500. package/dist/src/tools/mcp-client-manager.js +83 -0
  501. package/dist/src/tools/mcp-client-manager.js.map +1 -0
  502. package/dist/src/tools/mcp-client-manager.test.d.ts +6 -0
  503. package/dist/src/tools/mcp-client-manager.test.js +58 -0
  504. package/dist/src/tools/mcp-client-manager.test.js.map +1 -0
  505. package/dist/src/tools/mcp-client.d.ts +199 -0
  506. package/dist/src/tools/mcp-client.js +1000 -0
  507. package/dist/src/tools/mcp-client.js.map +1 -0
  508. package/dist/src/tools/mcp-client.test.d.ts +6 -0
  509. package/dist/src/tools/mcp-client.test.js +454 -0
  510. package/dist/src/tools/mcp-client.test.js.map +1 -0
  511. package/dist/src/tools/mcp-tool.d.ts +24 -0
  512. package/dist/src/tools/mcp-tool.js +240 -0
  513. package/dist/src/tools/mcp-tool.js.map +1 -0
  514. package/dist/src/tools/mcp-tool.test.d.ts +6 -0
  515. package/dist/src/tools/mcp-tool.test.js +597 -0
  516. package/dist/src/tools/mcp-tool.test.js.map +1 -0
  517. package/dist/src/tools/memoryTool.d.ts +40 -0
  518. package/dist/src/tools/memoryTool.js +268 -0
  519. package/dist/src/tools/memoryTool.js.map +1 -0
  520. package/dist/src/tools/memoryTool.test.d.ts +6 -0
  521. package/dist/src/tools/memoryTool.test.js +298 -0
  522. package/dist/src/tools/memoryTool.test.js.map +1 -0
  523. package/dist/src/tools/modifiable-tool.d.ts +32 -0
  524. package/dist/src/tools/modifiable-tool.js +88 -0
  525. package/dist/src/tools/modifiable-tool.js.map +1 -0
  526. package/dist/src/tools/modifiable-tool.test.d.ts +6 -0
  527. package/dist/src/tools/modifiable-tool.test.js +193 -0
  528. package/dist/src/tools/modifiable-tool.test.js.map +1 -0
  529. package/dist/src/tools/read-file.d.ts +35 -0
  530. package/dist/src/tools/read-file.js +132 -0
  531. package/dist/src/tools/read-file.js.map +1 -0
  532. package/dist/src/tools/read-file.test.d.ts +6 -0
  533. package/dist/src/tools/read-file.test.js +340 -0
  534. package/dist/src/tools/read-file.test.js.map +1 -0
  535. package/dist/src/tools/read-many-files.d.ts +60 -0
  536. package/dist/src/tools/read-many-files.js +382 -0
  537. package/dist/src/tools/read-many-files.js.map +1 -0
  538. package/dist/src/tools/read-many-files.test.d.ts +6 -0
  539. package/dist/src/tools/read-many-files.test.js +565 -0
  540. package/dist/src/tools/read-many-files.test.js.map +1 -0
  541. package/dist/src/tools/ripGrep.d.ts +55 -0
  542. package/dist/src/tools/ripGrep.js +393 -0
  543. package/dist/src/tools/ripGrep.js.map +1 -0
  544. package/dist/src/tools/ripGrep.test.d.ts +6 -0
  545. package/dist/src/tools/ripGrep.test.js +976 -0
  546. package/dist/src/tools/ripGrep.test.js.map +1 -0
  547. package/dist/src/tools/shell.d.ts +32 -0
  548. package/dist/src/tools/shell.js +316 -0
  549. package/dist/src/tools/shell.js.map +1 -0
  550. package/dist/src/tools/shell.test.d.ts +6 -0
  551. package/dist/src/tools/shell.test.js +301 -0
  552. package/dist/src/tools/shell.test.js.map +1 -0
  553. package/dist/src/tools/smart-edit.d.ts +72 -0
  554. package/dist/src/tools/smart-edit.js +594 -0
  555. package/dist/src/tools/smart-edit.js.map +1 -0
  556. package/dist/src/tools/smart-edit.test.d.ts +6 -0
  557. package/dist/src/tools/smart-edit.test.js +419 -0
  558. package/dist/src/tools/smart-edit.test.js.map +1 -0
  559. package/dist/src/tools/tool-error.d.ts +43 -0
  560. package/dist/src/tools/tool-error.js +58 -0
  561. package/dist/src/tools/tool-error.js.map +1 -0
  562. package/dist/src/tools/tool-registry.d.ts +87 -0
  563. package/dist/src/tools/tool-registry.js +370 -0
  564. package/dist/src/tools/tool-registry.js.map +1 -0
  565. package/dist/src/tools/tool-registry.test.d.ts +6 -0
  566. package/dist/src/tools/tool-registry.test.js +332 -0
  567. package/dist/src/tools/tool-registry.test.js.map +1 -0
  568. package/dist/src/tools/tools.d.ts +281 -0
  569. package/dist/src/tools/tools.js +257 -0
  570. package/dist/src/tools/tools.js.map +1 -0
  571. package/dist/src/tools/tools.test.d.ts +6 -0
  572. package/dist/src/tools/tools.test.js +205 -0
  573. package/dist/src/tools/tools.test.js.map +1 -0
  574. package/dist/src/tools/web-fetch.d.ts +27 -0
  575. package/dist/src/tools/web-fetch.js +244 -0
  576. package/dist/src/tools/web-fetch.js.map +1 -0
  577. package/dist/src/tools/web-fetch.test.d.ts +6 -0
  578. package/dist/src/tools/web-fetch.test.js +114 -0
  579. package/dist/src/tools/web-fetch.test.js.map +1 -0
  580. package/dist/src/tools/web-search.d.ts +49 -0
  581. package/dist/src/tools/web-search.js +139 -0
  582. package/dist/src/tools/web-search.js.map +1 -0
  583. package/dist/src/tools/web-search.test.d.ts +6 -0
  584. package/dist/src/tools/web-search.test.js +207 -0
  585. package/dist/src/tools/web-search.test.js.map +1 -0
  586. package/dist/src/tools/write-file.d.ts +52 -0
  587. package/dist/src/tools/write-file.js +309 -0
  588. package/dist/src/tools/write-file.js.map +1 -0
  589. package/dist/src/tools/write-file.test.d.ts +6 -0
  590. package/dist/src/tools/write-file.test.js +611 -0
  591. package/dist/src/tools/write-file.test.js.map +1 -0
  592. package/dist/src/utils/LruCache.d.ts +13 -0
  593. package/dist/src/utils/LruCache.js +38 -0
  594. package/dist/src/utils/LruCache.js.map +1 -0
  595. package/dist/src/utils/bfsFileSearch.d.ts +24 -0
  596. package/dist/src/utils/bfsFileSearch.js +95 -0
  597. package/dist/src/utils/bfsFileSearch.js.map +1 -0
  598. package/dist/src/utils/bfsFileSearch.test.d.ts +6 -0
  599. package/dist/src/utils/bfsFileSearch.test.js +163 -0
  600. package/dist/src/utils/bfsFileSearch.test.js.map +1 -0
  601. package/dist/src/utils/browser.d.ts +13 -0
  602. package/dist/src/utils/browser.js +50 -0
  603. package/dist/src/utils/browser.js.map +1 -0
  604. package/dist/src/utils/editCorrector.d.ts +54 -0
  605. package/dist/src/utils/editCorrector.js +588 -0
  606. package/dist/src/utils/editCorrector.js.map +1 -0
  607. package/dist/src/utils/editCorrector.test.d.ts +6 -0
  608. package/dist/src/utils/editCorrector.test.js +515 -0
  609. package/dist/src/utils/editCorrector.test.js.map +1 -0
  610. package/dist/src/utils/editor.d.ts +28 -0
  611. package/dist/src/utils/editor.js +173 -0
  612. package/dist/src/utils/editor.js.map +1 -0
  613. package/dist/src/utils/editor.test.d.ts +6 -0
  614. package/dist/src/utils/editor.test.js +431 -0
  615. package/dist/src/utils/editor.test.js.map +1 -0
  616. package/dist/src/utils/environmentContext.d.ts +21 -0
  617. package/dist/src/utils/environmentContext.js +90 -0
  618. package/dist/src/utils/environmentContext.js.map +1 -0
  619. package/dist/src/utils/environmentContext.test.d.ts +6 -0
  620. package/dist/src/utils/environmentContext.test.js +140 -0
  621. package/dist/src/utils/environmentContext.test.js.map +1 -0
  622. package/dist/src/utils/errorParsing.d.ts +8 -0
  623. package/dist/src/utils/errorParsing.js +93 -0
  624. package/dist/src/utils/errorParsing.js.map +1 -0
  625. package/dist/src/utils/errorParsing.test.d.ts +6 -0
  626. package/dist/src/utils/errorParsing.test.js +172 -0
  627. package/dist/src/utils/errorParsing.test.js.map +1 -0
  628. package/dist/src/utils/errorReporting.d.ts +14 -0
  629. package/dist/src/utils/errorReporting.js +88 -0
  630. package/dist/src/utils/errorReporting.js.map +1 -0
  631. package/dist/src/utils/errorReporting.test.d.ts +6 -0
  632. package/dist/src/utils/errorReporting.test.js +130 -0
  633. package/dist/src/utils/errorReporting.test.js.map +1 -0
  634. package/dist/src/utils/errors.d.ts +39 -0
  635. package/dist/src/utils/errors.js +96 -0
  636. package/dist/src/utils/errors.js.map +1 -0
  637. package/dist/src/utils/fetch.d.ts +11 -0
  638. package/dist/src/utils/fetch.js +51 -0
  639. package/dist/src/utils/fetch.js.map +1 -0
  640. package/dist/src/utils/fileUtils.d.ts +69 -0
  641. package/dist/src/utils/fileUtils.js +405 -0
  642. package/dist/src/utils/fileUtils.js.map +1 -0
  643. package/dist/src/utils/fileUtils.test.d.ts +6 -0
  644. package/dist/src/utils/fileUtils.test.js +682 -0
  645. package/dist/src/utils/fileUtils.test.js.map +1 -0
  646. package/dist/src/utils/filesearch/crawlCache.d.ts +25 -0
  647. package/dist/src/utils/filesearch/crawlCache.js +57 -0
  648. package/dist/src/utils/filesearch/crawlCache.js.map +1 -0
  649. package/dist/src/utils/filesearch/crawlCache.test.d.ts +6 -0
  650. package/dist/src/utils/filesearch/crawlCache.test.js +103 -0
  651. package/dist/src/utils/filesearch/crawlCache.test.js.map +1 -0
  652. package/dist/src/utils/filesearch/crawler.d.ts +15 -0
  653. package/dist/src/utils/filesearch/crawler.js +50 -0
  654. package/dist/src/utils/filesearch/crawler.js.map +1 -0
  655. package/dist/src/utils/filesearch/crawler.test.d.ts +6 -0
  656. package/dist/src/utils/filesearch/crawler.test.js +468 -0
  657. package/dist/src/utils/filesearch/crawler.test.js.map +1 -0
  658. package/dist/src/utils/filesearch/fileSearch.d.ts +38 -0
  659. package/dist/src/utils/filesearch/fileSearch.js +191 -0
  660. package/dist/src/utils/filesearch/fileSearch.js.map +1 -0
  661. package/dist/src/utils/filesearch/fileSearch.test.d.ts +6 -0
  662. package/dist/src/utils/filesearch/fileSearch.test.js +642 -0
  663. package/dist/src/utils/filesearch/fileSearch.test.js.map +1 -0
  664. package/dist/src/utils/filesearch/ignore.d.ts +42 -0
  665. package/dist/src/utils/filesearch/ignore.js +106 -0
  666. package/dist/src/utils/filesearch/ignore.js.map +1 -0
  667. package/dist/src/utils/filesearch/ignore.test.d.ts +6 -0
  668. package/dist/src/utils/filesearch/ignore.test.js +144 -0
  669. package/dist/src/utils/filesearch/ignore.test.js.map +1 -0
  670. package/dist/src/utils/filesearch/result-cache.d.ts +33 -0
  671. package/dist/src/utils/filesearch/result-cache.js +59 -0
  672. package/dist/src/utils/filesearch/result-cache.js.map +1 -0
  673. package/dist/src/utils/filesearch/result-cache.test.d.ts +6 -0
  674. package/dist/src/utils/filesearch/result-cache.test.js +46 -0
  675. package/dist/src/utils/filesearch/result-cache.test.js.map +1 -0
  676. package/dist/src/utils/flashFallback.test.d.ts +6 -0
  677. package/dist/src/utils/flashFallback.test.js +122 -0
  678. package/dist/src/utils/flashFallback.test.js.map +1 -0
  679. package/dist/src/utils/formatters.d.ts +6 -0
  680. package/dist/src/utils/formatters.js +16 -0
  681. package/dist/src/utils/formatters.js.map +1 -0
  682. package/dist/src/utils/geminiIgnoreParser.d.ts +18 -0
  683. package/dist/src/utils/geminiIgnoreParser.js +61 -0
  684. package/dist/src/utils/geminiIgnoreParser.js.map +1 -0
  685. package/dist/src/utils/geminiIgnoreParser.test.d.ts +6 -0
  686. package/dist/src/utils/geminiIgnoreParser.test.js +50 -0
  687. package/dist/src/utils/geminiIgnoreParser.test.js.map +1 -0
  688. package/dist/src/utils/generateContentResponseUtilities.d.ts +13 -0
  689. package/dist/src/utils/generateContentResponseUtilities.js +80 -0
  690. package/dist/src/utils/generateContentResponseUtilities.js.map +1 -0
  691. package/dist/src/utils/generateContentResponseUtilities.test.d.ts +6 -0
  692. package/dist/src/utils/generateContentResponseUtilities.test.js +235 -0
  693. package/dist/src/utils/generateContentResponseUtilities.test.js.map +1 -0
  694. package/dist/src/utils/getFolderStructure.d.ts +31 -0
  695. package/dist/src/utils/getFolderStructure.js +246 -0
  696. package/dist/src/utils/getFolderStructure.js.map +1 -0
  697. package/dist/src/utils/getFolderStructure.test.d.ts +6 -0
  698. package/dist/src/utils/getFolderStructure.test.js +282 -0
  699. package/dist/src/utils/getFolderStructure.test.js.map +1 -0
  700. package/dist/src/utils/getPty.d.ts +19 -0
  701. package/dist/src/utils/getPty.js +23 -0
  702. package/dist/src/utils/getPty.js.map +1 -0
  703. package/dist/src/utils/gitIgnoreParser.d.ts +16 -0
  704. package/dist/src/utils/gitIgnoreParser.js +152 -0
  705. package/dist/src/utils/gitIgnoreParser.js.map +1 -0
  706. package/dist/src/utils/gitIgnoreParser.test.d.ts +6 -0
  707. package/dist/src/utils/gitIgnoreParser.test.js +185 -0
  708. package/dist/src/utils/gitIgnoreParser.test.js.map +1 -0
  709. package/dist/src/utils/gitUtils.d.ts +17 -0
  710. package/dist/src/utils/gitUtils.js +61 -0
  711. package/dist/src/utils/gitUtils.js.map +1 -0
  712. package/dist/src/utils/ignorePatterns.d.ts +103 -0
  713. package/dist/src/utils/ignorePatterns.js +220 -0
  714. package/dist/src/utils/ignorePatterns.js.map +1 -0
  715. package/dist/src/utils/ignorePatterns.test.d.ts +6 -0
  716. package/dist/src/utils/ignorePatterns.test.js +250 -0
  717. package/dist/src/utils/ignorePatterns.test.js.map +1 -0
  718. package/dist/src/utils/installationManager.d.ts +16 -0
  719. package/dist/src/utils/installationManager.js +50 -0
  720. package/dist/src/utils/installationManager.js.map +1 -0
  721. package/dist/src/utils/installationManager.test.d.ts +6 -0
  722. package/dist/src/utils/installationManager.test.js +83 -0
  723. package/dist/src/utils/installationManager.test.js.map +1 -0
  724. package/dist/src/utils/language-detection.d.ts +6 -0
  725. package/dist/src/utils/language-detection.js +101 -0
  726. package/dist/src/utils/language-detection.js.map +1 -0
  727. package/dist/src/utils/llm-edit-fixer.d.ts +26 -0
  728. package/dist/src/utils/llm-edit-fixer.js +121 -0
  729. package/dist/src/utils/llm-edit-fixer.js.map +1 -0
  730. package/dist/src/utils/llm-edit-fixer.test.d.ts +6 -0
  731. package/dist/src/utils/llm-edit-fixer.test.js +105 -0
  732. package/dist/src/utils/llm-edit-fixer.test.js.map +1 -0
  733. package/dist/src/utils/memoryDiscovery.d.ts +16 -0
  734. package/dist/src/utils/memoryDiscovery.js +254 -0
  735. package/dist/src/utils/memoryDiscovery.js.map +1 -0
  736. package/dist/src/utils/memoryDiscovery.test.d.ts +6 -0
  737. package/dist/src/utils/memoryDiscovery.test.js +244 -0
  738. package/dist/src/utils/memoryDiscovery.test.js.map +1 -0
  739. package/dist/src/utils/memoryImportProcessor.d.ts +42 -0
  740. package/dist/src/utils/memoryImportProcessor.js +296 -0
  741. package/dist/src/utils/memoryImportProcessor.js.map +1 -0
  742. package/dist/src/utils/memoryImportProcessor.test.d.ts +6 -0
  743. package/dist/src/utils/memoryImportProcessor.test.js +573 -0
  744. package/dist/src/utils/memoryImportProcessor.test.js.map +1 -0
  745. package/dist/src/utils/messageInspectors.d.ts +8 -0
  746. package/dist/src/utils/messageInspectors.js +16 -0
  747. package/dist/src/utils/messageInspectors.js.map +1 -0
  748. package/dist/src/utils/nextSpeakerChecker.d.ts +12 -0
  749. package/dist/src/utils/nextSpeakerChecker.js +97 -0
  750. package/dist/src/utils/nextSpeakerChecker.js.map +1 -0
  751. package/dist/src/utils/nextSpeakerChecker.test.d.ts +6 -0
  752. package/dist/src/utils/nextSpeakerChecker.test.js +179 -0
  753. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -0
  754. package/dist/src/utils/partUtils.d.ts +35 -0
  755. package/dist/src/utils/partUtils.js +133 -0
  756. package/dist/src/utils/partUtils.js.map +1 -0
  757. package/dist/src/utils/partUtils.test.d.ts +6 -0
  758. package/dist/src/utils/partUtils.test.js +241 -0
  759. package/dist/src/utils/partUtils.test.js.map +1 -0
  760. package/dist/src/utils/pathReader.d.ts +17 -0
  761. package/dist/src/utils/pathReader.js +92 -0
  762. package/dist/src/utils/pathReader.js.map +1 -0
  763. package/dist/src/utils/pathReader.test.d.ts +6 -0
  764. package/dist/src/utils/pathReader.test.js +363 -0
  765. package/dist/src/utils/pathReader.test.js.map +1 -0
  766. package/dist/src/utils/paths.d.ts +58 -0
  767. package/dist/src/utils/paths.js +159 -0
  768. package/dist/src/utils/paths.js.map +1 -0
  769. package/dist/src/utils/paths.test.d.ts +6 -0
  770. package/dist/src/utils/paths.test.js +225 -0
  771. package/dist/src/utils/paths.test.js.map +1 -0
  772. package/dist/src/utils/promptIdContext.d.ts +7 -0
  773. package/dist/src/utils/promptIdContext.js +8 -0
  774. package/dist/src/utils/promptIdContext.js.map +1 -0
  775. package/dist/src/utils/quotaErrorDetection.d.ts +18 -0
  776. package/dist/src/utils/quotaErrorDetection.js +65 -0
  777. package/dist/src/utils/quotaErrorDetection.js.map +1 -0
  778. package/dist/src/utils/retry.d.ts +30 -0
  779. package/dist/src/utils/retry.js +276 -0
  780. package/dist/src/utils/retry.js.map +1 -0
  781. package/dist/src/utils/retry.test.d.ts +6 -0
  782. package/dist/src/utils/retry.test.js +325 -0
  783. package/dist/src/utils/retry.test.js.map +1 -0
  784. package/dist/src/utils/safeJsonStringify.d.ts +13 -0
  785. package/dist/src/utils/safeJsonStringify.js +25 -0
  786. package/dist/src/utils/safeJsonStringify.js.map +1 -0
  787. package/dist/src/utils/safeJsonStringify.test.d.ts +6 -0
  788. package/dist/src/utils/safeJsonStringify.test.js +61 -0
  789. package/dist/src/utils/safeJsonStringify.test.js.map +1 -0
  790. package/dist/src/utils/schemaValidator.d.ts +15 -0
  791. package/dist/src/utils/schemaValidator.js +38 -0
  792. package/dist/src/utils/schemaValidator.js.map +1 -0
  793. package/dist/src/utils/secure-browser-launcher.d.ts +23 -0
  794. package/dist/src/utils/secure-browser-launcher.js +165 -0
  795. package/dist/src/utils/secure-browser-launcher.js.map +1 -0
  796. package/dist/src/utils/secure-browser-launcher.test.d.ts +6 -0
  797. package/dist/src/utils/secure-browser-launcher.test.js +149 -0
  798. package/dist/src/utils/secure-browser-launcher.test.js.map +1 -0
  799. package/dist/src/utils/session.d.ts +6 -0
  800. package/dist/src/utils/session.js +8 -0
  801. package/dist/src/utils/session.js.map +1 -0
  802. package/dist/src/utils/shell-utils.d.ts +122 -0
  803. package/dist/src/utils/shell-utils.js +393 -0
  804. package/dist/src/utils/shell-utils.js.map +1 -0
  805. package/dist/src/utils/shell-utils.test.d.ts +6 -0
  806. package/dist/src/utils/shell-utils.test.js +332 -0
  807. package/dist/src/utils/shell-utils.test.js.map +1 -0
  808. package/dist/src/utils/summarizer.d.ts +25 -0
  809. package/dist/src/utils/summarizer.js +51 -0
  810. package/dist/src/utils/summarizer.js.map +1 -0
  811. package/dist/src/utils/summarizer.test.d.ts +6 -0
  812. package/dist/src/utils/summarizer.test.js +131 -0
  813. package/dist/src/utils/summarizer.test.js.map +1 -0
  814. package/dist/src/utils/systemEncoding.d.ts +40 -0
  815. package/dist/src/utils/systemEncoding.js +149 -0
  816. package/dist/src/utils/systemEncoding.js.map +1 -0
  817. package/dist/src/utils/systemEncoding.test.d.ts +6 -0
  818. package/dist/src/utils/systemEncoding.test.js +368 -0
  819. package/dist/src/utils/systemEncoding.test.js.map +1 -0
  820. package/dist/src/utils/terminalSerializer.d.ts +28 -0
  821. package/dist/src/utils/terminalSerializer.js +432 -0
  822. package/dist/src/utils/terminalSerializer.js.map +1 -0
  823. package/dist/src/utils/terminalSerializer.test.d.ts +6 -0
  824. package/dist/src/utils/terminalSerializer.test.js +176 -0
  825. package/dist/src/utils/terminalSerializer.test.js.map +1 -0
  826. package/dist/src/utils/testUtils.d.ts +29 -0
  827. package/dist/src/utils/testUtils.js +70 -0
  828. package/dist/src/utils/testUtils.js.map +1 -0
  829. package/dist/src/utils/textUtils.d.ts +18 -0
  830. package/dist/src/utils/textUtils.js +42 -0
  831. package/dist/src/utils/textUtils.js.map +1 -0
  832. package/dist/src/utils/textUtils.test.d.ts +6 -0
  833. package/dist/src/utils/textUtils.test.js +59 -0
  834. package/dist/src/utils/textUtils.test.js.map +1 -0
  835. package/dist/src/utils/tool-utils.d.ts +19 -0
  836. package/dist/src/utils/tool-utils.js +58 -0
  837. package/dist/src/utils/tool-utils.js.map +1 -0
  838. package/dist/src/utils/tool-utils.test.d.ts +6 -0
  839. package/dist/src/utils/tool-utils.test.js +61 -0
  840. package/dist/src/utils/tool-utils.test.js.map +1 -0
  841. package/dist/src/utils/userAccountManager.d.ts +20 -0
  842. package/dist/src/utils/userAccountManager.js +114 -0
  843. package/dist/src/utils/userAccountManager.js.map +1 -0
  844. package/dist/src/utils/userAccountManager.test.d.ts +6 -0
  845. package/dist/src/utils/userAccountManager.test.js +223 -0
  846. package/dist/src/utils/userAccountManager.test.js.map +1 -0
  847. package/dist/src/utils/workspaceContext.d.ts +66 -0
  848. package/dist/src/utils/workspaceContext.js +171 -0
  849. package/dist/src/utils/workspaceContext.js.map +1 -0
  850. package/dist/src/utils/workspaceContext.test.d.ts +6 -0
  851. package/dist/src/utils/workspaceContext.test.js +318 -0
  852. package/dist/src/utils/workspaceContext.test.js.map +1 -0
  853. package/dist/tsconfig.tsbuildinfo +1 -0
  854. package/package.json +91 -0
@@ -0,0 +1,1758 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
7
+ import { findCompressSplitPoint, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
8
+ import { AuthType, } from './contentGenerator.js';
9
+ import {} from './geminiChat.js';
10
+ import { CompressionStatus, GeminiEventType, Turn, } from './turn.js';
11
+ import { getCoreSystemPrompt } from './prompts.js';
12
+ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
13
+ import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
14
+ import { setSimulate429 } from '../utils/testUtils.js';
15
+ import { tokenLimit } from './tokenLimits.js';
16
+ import { ideContextStore } from '../ide/ideContext.js';
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
+ });
42
+ // --- Mocks ---
43
+ const mockTurnRunFn = vi.fn();
44
+ vi.mock('./turn', async (importOriginal) => {
45
+ const actual = await importOriginal();
46
+ // Define a mock class that has the same shape as the real Turn
47
+ class MockTurn {
48
+ pendingToolCalls = [];
49
+ // The run method is a property that holds our mock function
50
+ run = mockTurnRunFn;
51
+ constructor() {
52
+ // The constructor can be empty or do some mock setup
53
+ }
54
+ }
55
+ // Export the mock class as 'Turn'
56
+ return {
57
+ ...actual,
58
+ Turn: MockTurn,
59
+ };
60
+ });
61
+ vi.mock('../config/config.js');
62
+ vi.mock('./prompts');
63
+ vi.mock('../utils/getFolderStructure', () => ({
64
+ getFolderStructure: vi.fn().mockResolvedValue('Mock Folder Structure'),
65
+ }));
66
+ vi.mock('../utils/errorReporting', () => ({ reportError: vi.fn() }));
67
+ vi.mock('../utils/nextSpeakerChecker', () => ({
68
+ checkNextSpeaker: vi.fn().mockResolvedValue(null),
69
+ }));
70
+ vi.mock('../utils/generateContentResponseUtilities', () => ({
71
+ getResponseText: (result) => result.candidates?.[0]?.content?.parts?.map((part) => part.text).join('') ||
72
+ undefined,
73
+ }));
74
+ vi.mock('../telemetry/index.js', () => ({
75
+ logApiRequest: vi.fn(),
76
+ logApiResponse: vi.fn(),
77
+ logApiError: vi.fn(),
78
+ }));
79
+ vi.mock('../ide/ideContext.js');
80
+ vi.mock('../telemetry/uiTelemetry.js', () => ({
81
+ uiTelemetryService: {
82
+ setLastPromptTokenCount: vi.fn(),
83
+ },
84
+ }));
85
+ /**
86
+ * Array.fromAsync ponyfill, which will be available in es 2024.
87
+ *
88
+ * Buffers an async generator into an array and returns the result.
89
+ */
90
+ async function fromAsync(promise) {
91
+ const results = [];
92
+ for await (const result of promise) {
93
+ results.push(result);
94
+ }
95
+ return results;
96
+ }
97
+ describe('findIndexAfterFraction', () => {
98
+ it('should throw an error for non-positive numbers', () => {
99
+ expect(() => findCompressSplitPoint([], 0)).toThrow('Fraction must be between 0 and 1');
100
+ });
101
+ it('should throw an error for a fraction greater than or equal to 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);
106
+ });
107
+ it('should handle a fraction in the middle', () => {
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);
116
+ });
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);
126
+ });
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);
145
+ });
146
+ it('should handle a history with only one item', () => {
147
+ const historyWithEmptyParts = [
148
+ { role: 'user', parts: [{ text: 'Message 1' }] },
149
+ ];
150
+ expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(0);
151
+ });
152
+ it('should handle history with weird parts', () => {
153
+ const historyWithEmptyParts = [
154
+ { role: 'user', parts: [{ text: 'Message 1' }] },
155
+ { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
156
+ { role: 'user', parts: [{ text: 'Message 2' }] },
157
+ ];
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);
186
+ });
187
+ });
188
+ describe('Gemini Client (client.ts)', () => {
189
+ let mockContentGenerator;
190
+ let mockConfig;
191
+ let client;
192
+ let mockGenerateContentFn;
193
+ beforeEach(async () => {
194
+ vi.resetAllMocks();
195
+ vi.mocked(uiTelemetryService.setLastPromptTokenCount).mockClear();
196
+ mockGenerateContentFn = vi.fn().mockResolvedValue({
197
+ candidates: [{ content: { parts: [{ text: '{"key": "value"}' }] } }],
198
+ });
199
+ // Disable 429 simulation for tests
200
+ setSimulate429(false);
201
+ mockContentGenerator = {
202
+ generateContent: mockGenerateContentFn,
203
+ generateContentStream: vi.fn(),
204
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
205
+ batchEmbedContents: vi.fn(),
206
+ };
207
+ // Because the GeminiClient constructor kicks off an async process (startChat)
208
+ // that depends on a fully-formed Config object, we need to mock the
209
+ // entire implementation of Config for these tests.
210
+ const mockToolRegistry = {
211
+ getFunctionDeclarations: vi.fn().mockReturnValue([]),
212
+ getTool: vi.fn().mockReturnValue(null),
213
+ };
214
+ const fileService = new FileDiscoveryService('/test/dir');
215
+ const contentGeneratorConfig = {
216
+ apiKey: 'test-key',
217
+ vertexai: false,
218
+ authType: AuthType.USE_GEMINI,
219
+ };
220
+ mockConfig = {
221
+ getContentGeneratorConfig: vi
222
+ .fn()
223
+ .mockReturnValue(contentGeneratorConfig),
224
+ getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
225
+ getModel: vi.fn().mockReturnValue('test-model'),
226
+ getEmbeddingModel: vi.fn().mockReturnValue('test-embedding-model'),
227
+ getApiKey: vi.fn().mockReturnValue('test-key'),
228
+ getVertexAI: vi.fn().mockReturnValue(false),
229
+ getUserAgent: vi.fn().mockReturnValue('test-agent'),
230
+ getUserMemory: vi.fn().mockReturnValue(''),
231
+ getFullContext: vi.fn().mockReturnValue(false),
232
+ getSessionId: vi.fn().mockReturnValue('test-session-id'),
233
+ getProxy: vi.fn().mockReturnValue(undefined),
234
+ getWorkingDir: vi.fn().mockReturnValue('/test/dir'),
235
+ getFileService: vi.fn().mockReturnValue(fileService),
236
+ getMaxSessionTurns: vi.fn().mockReturnValue(0),
237
+ getQuotaErrorOccurred: vi.fn().mockReturnValue(false),
238
+ setQuotaErrorOccurred: vi.fn(),
239
+ getNoBrowser: vi.fn().mockReturnValue(false),
240
+ getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
241
+ getIdeModeFeature: vi.fn().mockReturnValue(false),
242
+ getIdeMode: vi.fn().mockReturnValue(true),
243
+ getDebugMode: vi.fn().mockReturnValue(false),
244
+ getWorkspaceContext: vi.fn().mockReturnValue({
245
+ getDirectories: vi.fn().mockReturnValue(['/test/dir']),
246
+ }),
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),
252
+ setFallbackMode: vi.fn(),
253
+ getChatCompression: vi.fn().mockReturnValue(undefined),
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
+ }),
268
+ };
269
+ client = new GeminiClient(mockConfig);
270
+ await client.initialize();
271
+ vi.mocked(mockConfig.getGeminiClient).mockReturnValue(client);
272
+ });
273
+ afterEach(() => {
274
+ vi.restoreAllMocks();
275
+ });
276
+ describe('addHistory', () => {
277
+ it('should call chat.addHistory with the provided content', async () => {
278
+ const mockChat = {
279
+ addHistory: vi.fn(),
280
+ };
281
+ client['chat'] = mockChat;
282
+ const newContent = {
283
+ role: 'user',
284
+ parts: [{ text: 'New history item' }],
285
+ };
286
+ await client.addHistory(newContent);
287
+ expect(mockChat.addHistory).toHaveBeenCalledWith(newContent);
288
+ });
289
+ });
290
+ describe('resetChat', () => {
291
+ it('should create a new chat session, clearing the old history', async () => {
292
+ // 1. Get the initial chat instance and add some history.
293
+ const initialChat = client.getChat();
294
+ const initialHistory = await client.getHistory();
295
+ await client.addHistory({
296
+ role: 'user',
297
+ parts: [{ text: 'some old message' }],
298
+ });
299
+ const historyWithOldMessage = await client.getHistory();
300
+ expect(historyWithOldMessage.length).toBeGreaterThan(initialHistory.length);
301
+ // 2. Call resetChat.
302
+ await client.resetChat();
303
+ // 3. Get the new chat instance and its history.
304
+ const newChat = client.getChat();
305
+ const newHistory = await client.getHistory();
306
+ // 4. Assert that the chat instance is new and the history is reset.
307
+ expect(newChat).not.toBe(initialChat);
308
+ expect(newHistory.length).toBe(initialHistory.length);
309
+ expect(JSON.stringify(newHistory)).not.toContain('some old message');
310
+ });
311
+ });
312
+ describe('tryCompressChat', () => {
313
+ const mockGetHistory = vi.fn();
314
+ beforeEach(() => {
315
+ vi.mock('./tokenLimits', () => ({
316
+ tokenLimit: vi.fn(),
317
+ }));
318
+ client['chat'] = {
319
+ getHistory: mockGetHistory,
320
+ addHistory: vi.fn(),
321
+ setHistory: vi.fn(),
322
+ };
323
+ });
324
+ function setup({ chatHistory = [
325
+ { role: 'user', parts: [{ text: 'Long conversation' }] },
326
+ { role: 'model', parts: [{ text: 'Long response' }] },
327
+ ], } = {}) {
328
+ const mockChat = {
329
+ getHistory: vi.fn().mockReturnValue(chatHistory),
330
+ setHistory: vi.fn(),
331
+ };
332
+ vi.mocked(mockContentGenerator.countTokens)
333
+ .mockResolvedValueOnce({ totalTokens: 1000 })
334
+ .mockResolvedValueOnce({ totalTokens: 5000 });
335
+ client['chat'] = mockChat;
336
+ client['startChat'] = vi.fn().mockResolvedValue({ ...mockChat });
337
+ return { client, mockChat };
338
+ }
339
+ describe('when compression inflates the token count', () => {
340
+ it('allows compression to be forced/manual after a failure', async () => {
341
+ const { client } = setup();
342
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
343
+ totalTokens: 1000,
344
+ });
345
+ await client.tryCompressChat('prompt-id-4', false); // Fails
346
+ const result = await client.tryCompressChat('prompt-id-4', true);
347
+ expect(result).toEqual({
348
+ compressionStatus: CompressionStatus.COMPRESSED,
349
+ newTokenCount: 1000,
350
+ originalTokenCount: 1000,
351
+ });
352
+ });
353
+ it('yields the result even if the compression inflated the tokens', async () => {
354
+ const { client } = setup();
355
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
356
+ totalTokens: 1000,
357
+ });
358
+ const result = await client.tryCompressChat('prompt-id-4', false);
359
+ expect(result).toEqual({
360
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
361
+ newTokenCount: 5000,
362
+ originalTokenCount: 1000,
363
+ });
364
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(5000);
365
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
366
+ });
367
+ it('does not manipulate the source chat', async () => {
368
+ const { client, mockChat } = setup();
369
+ await client.tryCompressChat('prompt-id-4', false);
370
+ expect(client['chat']).toBe(mockChat); // a new chat session was not created
371
+ });
372
+ it('restores the history back to the original', async () => {
373
+ vi.mocked(tokenLimit).mockReturnValue(1000);
374
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
375
+ totalTokens: 999,
376
+ });
377
+ const originalHistory = [
378
+ { role: 'user', parts: [{ text: 'what is your wisdom?' }] },
379
+ { role: 'model', parts: [{ text: 'some wisdom' }] },
380
+ { role: 'user', parts: [{ text: 'ahh that is a good a wisdom' }] },
381
+ ];
382
+ const { client } = setup({
383
+ chatHistory: originalHistory,
384
+ });
385
+ const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false);
386
+ expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
387
+ expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
388
+ });
389
+ it('will not attempt to compress context after a failure', async () => {
390
+ const { client } = setup();
391
+ await client.tryCompressChat('prompt-id-4', false);
392
+ const result = await client.tryCompressChat('prompt-id-5', false);
393
+ // it counts tokens for {original, compressed} and then never again
394
+ expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
395
+ expect(result).toEqual({
396
+ compressionStatus: CompressionStatus.NOOP,
397
+ newTokenCount: 0,
398
+ originalTokenCount: 0,
399
+ });
400
+ });
401
+ });
402
+ it('should not trigger summarization if token count is below threshold', async () => {
403
+ const MOCKED_TOKEN_LIMIT = 1000;
404
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
405
+ mockGetHistory.mockReturnValue([
406
+ { role: 'user', parts: [{ text: '...history...' }] },
407
+ ]);
408
+ vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
409
+ totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
410
+ });
411
+ const initialChat = client.getChat();
412
+ const result = await client.tryCompressChat('prompt-id-2', false);
413
+ const newChat = client.getChat();
414
+ expect(tokenLimit).toHaveBeenCalled();
415
+ expect(result).toEqual({
416
+ compressionStatus: CompressionStatus.NOOP,
417
+ newTokenCount: 699,
418
+ originalTokenCount: 699,
419
+ });
420
+ expect(newChat).toBe(initialChat);
421
+ });
422
+ it('logs a telemetry event when compressing', async () => {
423
+ vi.spyOn(ClearcutLogger.prototype, 'logChatCompressionEvent');
424
+ const MOCKED_TOKEN_LIMIT = 1000;
425
+ const MOCKED_CONTEXT_PERCENTAGE_THRESHOLD = 0.5;
426
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
427
+ vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
428
+ contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
429
+ });
430
+ mockGetHistory.mockReturnValue([
431
+ { role: 'user', parts: [{ text: '...history...' }] },
432
+ ]);
433
+ const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
434
+ const newTokenCount = 100;
435
+ vi.mocked(mockContentGenerator.countTokens)
436
+ .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
437
+ .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
438
+ // Mock the summary response from the chat
439
+ mockGenerateContentFn.mockResolvedValue({
440
+ candidates: [
441
+ {
442
+ content: {
443
+ role: 'model',
444
+ parts: [{ text: 'This is a summary.' }],
445
+ },
446
+ },
447
+ ],
448
+ });
449
+ await client.tryCompressChat('prompt-id-3', false);
450
+ expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
451
+ tokens_before: originalTokenCount,
452
+ tokens_after: newTokenCount,
453
+ }));
454
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(newTokenCount);
455
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
456
+ });
457
+ it('should trigger summarization if token count is at threshold with contextPercentageThreshold setting', async () => {
458
+ const MOCKED_TOKEN_LIMIT = 1000;
459
+ const MOCKED_CONTEXT_PERCENTAGE_THRESHOLD = 0.5;
460
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
461
+ vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
462
+ contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
463
+ });
464
+ mockGetHistory.mockReturnValue([
465
+ { role: 'user', parts: [{ text: '...history...' }] },
466
+ ]);
467
+ const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
468
+ const newTokenCount = 100;
469
+ vi.mocked(mockContentGenerator.countTokens)
470
+ .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
471
+ .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
472
+ // Mock the summary response from the chat
473
+ mockGenerateContentFn.mockResolvedValue({
474
+ candidates: [
475
+ {
476
+ content: {
477
+ role: 'model',
478
+ parts: [{ text: 'This is a summary.' }],
479
+ },
480
+ },
481
+ ],
482
+ });
483
+ const initialChat = client.getChat();
484
+ const result = await client.tryCompressChat('prompt-id-3', false);
485
+ const newChat = client.getChat();
486
+ expect(tokenLimit).toHaveBeenCalled();
487
+ expect(mockGenerateContentFn).toHaveBeenCalled();
488
+ // Assert that summarization happened and returned the correct stats
489
+ expect(result).toEqual({
490
+ compressionStatus: CompressionStatus.COMPRESSED,
491
+ originalTokenCount,
492
+ newTokenCount,
493
+ });
494
+ // Assert that the chat was reset
495
+ expect(newChat).not.toBe(initialChat);
496
+ });
497
+ it('should not compress across a function call response', async () => {
498
+ const MOCKED_TOKEN_LIMIT = 1000;
499
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
500
+ mockGetHistory.mockReturnValue([
501
+ { role: 'user', parts: [{ text: '...history 1...' }] },
502
+ { role: 'model', parts: [{ text: '...history 2...' }] },
503
+ { role: 'user', parts: [{ text: '...history 3...' }] },
504
+ { role: 'model', parts: [{ text: '...history 4...' }] },
505
+ { role: 'user', parts: [{ text: '...history 5...' }] },
506
+ { role: 'model', parts: [{ text: '...history 6...' }] },
507
+ { role: 'user', parts: [{ text: '...history 7...' }] },
508
+ { role: 'model', parts: [{ text: '...history 8...' }] },
509
+ // Normally we would break here, but we have a function response.
510
+ {
511
+ role: 'user',
512
+ parts: [{ functionResponse: { name: '...history 8...' } }],
513
+ },
514
+ { role: 'model', parts: [{ text: '...history 10...' }] },
515
+ // Instead we will break here.
516
+ { role: 'user', parts: [{ text: '...history 10...' }] },
517
+ ]);
518
+ const originalTokenCount = 1000 * 0.7;
519
+ const newTokenCount = 100;
520
+ vi.mocked(mockContentGenerator.countTokens)
521
+ .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
522
+ .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
523
+ // Mock the summary response from the chat
524
+ mockGenerateContentFn.mockResolvedValue({
525
+ candidates: [
526
+ {
527
+ content: {
528
+ role: 'model',
529
+ parts: [{ text: 'This is a summary.' }],
530
+ },
531
+ },
532
+ ],
533
+ });
534
+ const initialChat = client.getChat();
535
+ const result = await client.tryCompressChat('prompt-id-3', false);
536
+ const newChat = client.getChat();
537
+ expect(tokenLimit).toHaveBeenCalled();
538
+ expect(mockGenerateContentFn).toHaveBeenCalled();
539
+ // Assert that summarization happened and returned the correct stats
540
+ expect(result).toEqual({
541
+ compressionStatus: CompressionStatus.COMPRESSED,
542
+ originalTokenCount,
543
+ newTokenCount,
544
+ });
545
+ // Assert that the chat was reset
546
+ expect(newChat).not.toBe(initialChat);
547
+ // 1. standard start context message
548
+ // 2. standard canned user start message
549
+ // 3. compressed summary message
550
+ // 4. standard canned user summary message
551
+ // 5. The last user message (not the last 3 because that would start with a function response)
552
+ expect(newChat.getHistory().length).toEqual(5);
553
+ });
554
+ it('should always trigger summarization when force is true, regardless of token count', async () => {
555
+ mockGetHistory.mockReturnValue([
556
+ { role: 'user', parts: [{ text: '...history...' }] },
557
+ ]);
558
+ const originalTokenCount = 10; // Well below threshold
559
+ const newTokenCount = 5;
560
+ vi.mocked(mockContentGenerator.countTokens)
561
+ .mockResolvedValueOnce({ totalTokens: originalTokenCount })
562
+ .mockResolvedValueOnce({ totalTokens: newTokenCount });
563
+ // Mock the summary response from the chat
564
+ mockGenerateContentFn.mockResolvedValue({
565
+ candidates: [
566
+ {
567
+ content: {
568
+ role: 'model',
569
+ parts: [{ text: 'This is a summary.' }],
570
+ },
571
+ },
572
+ ],
573
+ });
574
+ const initialChat = client.getChat();
575
+ const result = await client.tryCompressChat('prompt-id-1', false); // force = true
576
+ const newChat = client.getChat();
577
+ expect(mockGenerateContentFn).toHaveBeenCalled();
578
+ expect(result).toEqual({
579
+ compressionStatus: CompressionStatus.COMPRESSED,
580
+ originalTokenCount,
581
+ newTokenCount,
582
+ });
583
+ // Assert that the chat was reset
584
+ expect(newChat).not.toBe(initialChat);
585
+ });
586
+ it('should use current model from config for token counting after sendMessage', async () => {
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)
595
+ .mockResolvedValueOnce({ totalTokens: 100000 })
596
+ .mockResolvedValueOnce({ totalTokens: 5000 });
597
+ const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
598
+ const mockChatHistory = [
599
+ { role: 'user', parts: [{ text: 'Long conversation' }] },
600
+ { role: 'model', parts: [{ text: 'Long response' }] },
601
+ ];
602
+ const mockChat = {
603
+ getHistory: vi.fn().mockImplementation(() => [...mockChatHistory]),
604
+ setHistory: vi.fn(),
605
+ sendMessage: mockSendMessage,
606
+ };
607
+ client['chat'] = mockChat;
608
+ client['startChat'] = vi.fn().mockResolvedValue(mockChat);
609
+ const result = await client.tryCompressChat('prompt-id-4', false);
610
+ expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
611
+ expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(1, {
612
+ model: firstCurrentModel,
613
+ contents: [...mockChatHistory],
614
+ });
615
+ expect(mockContentGenerator.countTokens).toHaveBeenNthCalledWith(2, {
616
+ model: secondCurrentModel,
617
+ contents: expect.any(Array),
618
+ });
619
+ expect(result).toEqual({
620
+ compressionStatus: CompressionStatus.COMPRESSED,
621
+ originalTokenCount: 100000,
622
+ newTokenCount: 5000,
623
+ });
624
+ });
625
+ });
626
+ describe('sendMessageStream', () => {
627
+ it('emits a compression event when the context was automatically compressed', async () => {
628
+ // Arrange
629
+ mockTurnRunFn.mockReturnValue((async function* () {
630
+ yield { type: 'content', value: 'Hello' };
631
+ })());
632
+ const compressionInfo = {
633
+ compressionStatus: CompressionStatus.COMPRESSED,
634
+ originalTokenCount: 1000,
635
+ newTokenCount: 500,
636
+ };
637
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValueOnce(compressionInfo);
638
+ // Act
639
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
640
+ const events = await fromAsync(stream);
641
+ // Assert
642
+ expect(events).toContainEqual({
643
+ type: GeminiEventType.ChatCompressed,
644
+ value: compressionInfo,
645
+ });
646
+ });
647
+ it.each([
648
+ {
649
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
650
+ },
651
+ { compressionStatus: CompressionStatus.NOOP },
652
+ {
653
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
654
+ },
655
+ ])('does not emit a compression event when the status is $compressionStatus', async ({ compressionStatus }) => {
656
+ // Arrange
657
+ const mockStream = (async function* () {
658
+ yield { type: 'content', value: 'Hello' };
659
+ })();
660
+ mockTurnRunFn.mockReturnValue(mockStream);
661
+ const compressionInfo = {
662
+ compressionStatus,
663
+ originalTokenCount: 1000,
664
+ newTokenCount: 500,
665
+ };
666
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValueOnce(compressionInfo);
667
+ // Act
668
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
669
+ const events = await fromAsync(stream);
670
+ // Assert
671
+ expect(events).not.toContainEqual({
672
+ type: GeminiEventType.ChatCompressed,
673
+ value: expect.anything(),
674
+ });
675
+ });
676
+ it('should include editor context when ideMode is enabled', async () => {
677
+ // Arrange
678
+ vi.mocked(ideContextStore.get).mockReturnValue({
679
+ workspaceState: {
680
+ openFiles: [
681
+ {
682
+ path: '/path/to/active/file.ts',
683
+ timestamp: Date.now(),
684
+ isActive: true,
685
+ selectedText: 'hello',
686
+ cursor: { line: 5, character: 10 },
687
+ },
688
+ {
689
+ path: '/path/to/recent/file1.ts',
690
+ timestamp: Date.now(),
691
+ },
692
+ {
693
+ path: '/path/to/recent/file2.ts',
694
+ timestamp: Date.now(),
695
+ },
696
+ ],
697
+ },
698
+ });
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* () {
706
+ yield { type: 'content', value: 'Hello' };
707
+ })());
708
+ const mockChat = {
709
+ addHistory: vi.fn(),
710
+ getHistory: vi.fn().mockReturnValue([]),
711
+ };
712
+ client['chat'] = mockChat;
713
+ const initialRequest = [{ text: 'Hi' }];
714
+ // Act
715
+ const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
716
+ for await (const _ of stream) {
717
+ // consume stream
718
+ }
719
+ // Assert
720
+ expect(ideContextStore.get).toHaveBeenCalled();
721
+ const expectedContext = `
722
+ Here is the user's editor context as a JSON object. This is for your information only.
723
+ \`\`\`json
724
+ ${JSON.stringify({
725
+ activeFile: {
726
+ path: '/path/to/active/file.ts',
727
+ cursor: {
728
+ line: 5,
729
+ character: 10,
730
+ },
731
+ selectedText: 'hello',
732
+ },
733
+ otherOpenFiles: ['/path/to/recent/file1.ts', '/path/to/recent/file2.ts'],
734
+ }, null, 2)}
735
+ \`\`\`
736
+ `.trim();
737
+ const expectedRequest = [{ text: expectedContext }];
738
+ expect(mockChat.addHistory).toHaveBeenCalledWith({
739
+ role: 'user',
740
+ parts: expectedRequest,
741
+ });
742
+ });
743
+ it('should not add context if ideMode is enabled but no open files', async () => {
744
+ // Arrange
745
+ vi.mocked(ideContextStore.get).mockReturnValue({
746
+ workspaceState: {
747
+ openFiles: [],
748
+ },
749
+ });
750
+ vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
751
+ const mockStream = (async function* () {
752
+ yield { type: 'content', value: 'Hello' };
753
+ })();
754
+ mockTurnRunFn.mockReturnValue(mockStream);
755
+ const mockChat = {
756
+ addHistory: vi.fn(),
757
+ getHistory: vi.fn().mockReturnValue([]),
758
+ };
759
+ client['chat'] = mockChat;
760
+ const initialRequest = [{ text: 'Hi' }];
761
+ // Act
762
+ const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
763
+ for await (const _ of stream) {
764
+ // consume stream
765
+ }
766
+ // Assert
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));
773
+ });
774
+ it('should add context if ideMode is enabled and there is one active file', async () => {
775
+ // Arrange
776
+ vi.mocked(ideContextStore.get).mockReturnValue({
777
+ workspaceState: {
778
+ openFiles: [
779
+ {
780
+ path: '/path/to/active/file.ts',
781
+ timestamp: Date.now(),
782
+ isActive: true,
783
+ selectedText: 'hello',
784
+ cursor: { line: 5, character: 10 },
785
+ },
786
+ ],
787
+ },
788
+ });
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
+ });
795
+ const mockStream = (async function* () {
796
+ yield { type: 'content', value: 'Hello' };
797
+ })();
798
+ mockTurnRunFn.mockReturnValue(mockStream);
799
+ const mockChat = {
800
+ addHistory: vi.fn(),
801
+ getHistory: vi.fn().mockReturnValue([]),
802
+ };
803
+ client['chat'] = mockChat;
804
+ const initialRequest = [{ text: 'Hi' }];
805
+ // Act
806
+ const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
807
+ for await (const _ of stream) {
808
+ // consume stream
809
+ }
810
+ // Assert
811
+ expect(ideContextStore.get).toHaveBeenCalled();
812
+ const expectedContext = `
813
+ Here is the user's editor context as a JSON object. This is for your information only.
814
+ \`\`\`json
815
+ ${JSON.stringify({
816
+ activeFile: {
817
+ path: '/path/to/active/file.ts',
818
+ cursor: {
819
+ line: 5,
820
+ character: 10,
821
+ },
822
+ selectedText: 'hello',
823
+ },
824
+ }, null, 2)}
825
+ \`\`\`
826
+ `.trim();
827
+ const expectedRequest = [{ text: expectedContext }];
828
+ expect(mockChat.addHistory).toHaveBeenCalledWith({
829
+ role: 'user',
830
+ parts: expectedRequest,
831
+ });
832
+ });
833
+ it('should add context if ideMode is enabled and there are open files but no active file', async () => {
834
+ // Arrange
835
+ vi.mocked(ideContextStore.get).mockReturnValue({
836
+ workspaceState: {
837
+ openFiles: [
838
+ {
839
+ path: '/path/to/recent/file1.ts',
840
+ timestamp: Date.now(),
841
+ },
842
+ {
843
+ path: '/path/to/recent/file2.ts',
844
+ timestamp: Date.now(),
845
+ },
846
+ ],
847
+ },
848
+ });
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
+ });
855
+ const mockStream = (async function* () {
856
+ yield { type: 'content', value: 'Hello' };
857
+ })();
858
+ mockTurnRunFn.mockReturnValue(mockStream);
859
+ const mockChat = {
860
+ addHistory: vi.fn(),
861
+ getHistory: vi.fn().mockReturnValue([]),
862
+ };
863
+ client['chat'] = mockChat;
864
+ const initialRequest = [{ text: 'Hi' }];
865
+ // Act
866
+ const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
867
+ for await (const _ of stream) {
868
+ // consume stream
869
+ }
870
+ // Assert
871
+ expect(ideContextStore.get).toHaveBeenCalled();
872
+ const expectedContext = `
873
+ Here is the user's editor context as a JSON object. This is for your information only.
874
+ \`\`\`json
875
+ ${JSON.stringify({
876
+ otherOpenFiles: ['/path/to/recent/file1.ts', '/path/to/recent/file2.ts'],
877
+ }, null, 2)}
878
+ \`\`\`
879
+ `.trim();
880
+ const expectedRequest = [{ text: expectedContext }];
881
+ expect(mockChat.addHistory).toHaveBeenCalledWith({
882
+ role: 'user',
883
+ parts: expectedRequest,
884
+ });
885
+ });
886
+ it('should return the turn instance after the stream is complete', async () => {
887
+ // Arrange
888
+ const mockStream = (async function* () {
889
+ yield { type: 'content', value: 'Hello' };
890
+ })();
891
+ mockTurnRunFn.mockReturnValue(mockStream);
892
+ const mockChat = {
893
+ addHistory: vi.fn(),
894
+ getHistory: vi.fn().mockReturnValue([]),
895
+ };
896
+ client['chat'] = mockChat;
897
+ // Act
898
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
899
+ // Consume the stream manually to get the final return value.
900
+ let finalResult;
901
+ while (true) {
902
+ const result = await stream.next();
903
+ if (result.done) {
904
+ finalResult = result.value;
905
+ break;
906
+ }
907
+ }
908
+ // Assert
909
+ expect(finalResult).toBeInstanceOf(Turn);
910
+ });
911
+ it('should stop infinite loop after MAX_TURNS when nextSpeaker always returns model', async () => {
912
+ // Get the mocked checkNextSpeaker function and configure it to trigger infinite loop
913
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
914
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
915
+ mockCheckNextSpeaker.mockResolvedValue({
916
+ next_speaker: 'model',
917
+ reasoning: 'Test case - always continue',
918
+ });
919
+ // Mock Turn to have no pending tool calls (which would allow nextSpeaker check)
920
+ const mockStream = (async function* () {
921
+ yield { type: 'content', value: 'Continue...' };
922
+ })();
923
+ mockTurnRunFn.mockReturnValue(mockStream);
924
+ const mockChat = {
925
+ addHistory: vi.fn(),
926
+ getHistory: vi.fn().mockReturnValue([]),
927
+ };
928
+ client['chat'] = mockChat;
929
+ // Use a signal that never gets aborted
930
+ const abortController = new AbortController();
931
+ const signal = abortController.signal;
932
+ // Act - Start the stream that should loop
933
+ const stream = client.sendMessageStream([{ text: 'Start conversation' }], signal, 'prompt-id-2');
934
+ // Count how many stream events we get
935
+ let eventCount = 0;
936
+ let finalResult;
937
+ // Consume the stream and count iterations
938
+ while (true) {
939
+ const result = await stream.next();
940
+ if (result.done) {
941
+ finalResult = result.value;
942
+ break;
943
+ }
944
+ eventCount++;
945
+ // Safety check to prevent actual infinite loop in test
946
+ if (eventCount > 200) {
947
+ abortController.abort();
948
+ throw new Error('Test exceeded expected event limit - possible actual infinite loop');
949
+ }
950
+ }
951
+ // Assert
952
+ expect(finalResult).toBeInstanceOf(Turn);
953
+ // Debug: Check how many times checkNextSpeaker was called
954
+ const callCount = mockCheckNextSpeaker.mock.calls.length;
955
+ // If infinite loop protection is working, checkNextSpeaker should be called many times
956
+ // but stop at MAX_TURNS (100). Since each recursive call should trigger checkNextSpeaker,
957
+ // we expect it to be called multiple times before hitting the limit
958
+ expect(mockCheckNextSpeaker).toHaveBeenCalled();
959
+ // The test should demonstrate that the infinite loop protection works:
960
+ // - If checkNextSpeaker is called many times (close to MAX_TURNS), it shows the loop was happening
961
+ // - If it's only called once, the recursive behavior might not be triggered
962
+ if (callCount === 0) {
963
+ throw new Error('checkNextSpeaker was never called - the recursive condition was not met');
964
+ }
965
+ else if (callCount === 1) {
966
+ // This might be expected behavior if the turn has pending tool calls or other conditions prevent recursion
967
+ console.log('checkNextSpeaker called only once - no infinite loop occurred');
968
+ }
969
+ else {
970
+ console.log(`checkNextSpeaker called ${callCount} times - infinite loop protection worked`);
971
+ // If called multiple times, we expect it to be stopped before MAX_TURNS
972
+ expect(callCount).toBeLessThanOrEqual(100); // Should not exceed MAX_TURNS
973
+ }
974
+ // The stream should produce events and eventually terminate
975
+ expect(eventCount).toBeGreaterThanOrEqual(1);
976
+ expect(eventCount).toBeLessThan(200); // Should not exceed our safety limit
977
+ });
978
+ it('should yield MaxSessionTurns and stop when session turn limit is reached', async () => {
979
+ // Arrange
980
+ const MAX_SESSION_TURNS = 5;
981
+ vi.spyOn(client['config'], 'getMaxSessionTurns').mockReturnValue(MAX_SESSION_TURNS);
982
+ const mockStream = (async function* () {
983
+ yield { type: 'content', value: 'Hello' };
984
+ })();
985
+ mockTurnRunFn.mockReturnValue(mockStream);
986
+ const mockChat = {
987
+ addHistory: vi.fn(),
988
+ getHistory: vi.fn().mockReturnValue([]),
989
+ };
990
+ client['chat'] = mockChat;
991
+ // Act & Assert
992
+ // Run up to the limit
993
+ for (let i = 0; i < MAX_SESSION_TURNS; i++) {
994
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-4');
995
+ // consume stream
996
+ for await (const _event of stream) {
997
+ // do nothing
998
+ }
999
+ }
1000
+ // This call should exceed the limit
1001
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-5');
1002
+ const events = [];
1003
+ for await (const event of stream) {
1004
+ events.push(event);
1005
+ }
1006
+ expect(events).toEqual([{ type: GeminiEventType.MaxSessionTurns }]);
1007
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(MAX_SESSION_TURNS);
1008
+ });
1009
+ it('should respect MAX_TURNS limit even when turns parameter is set to a large value', async () => {
1010
+ // This test verifies that the infinite loop protection works even when
1011
+ // someone tries to bypass it by calling with a very large turns value
1012
+ // Get the mocked checkNextSpeaker function and configure it to trigger infinite loop
1013
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
1014
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
1015
+ mockCheckNextSpeaker.mockResolvedValue({
1016
+ next_speaker: 'model',
1017
+ reasoning: 'Test case - always continue',
1018
+ });
1019
+ // Mock Turn to have no pending tool calls (which would allow nextSpeaker check)
1020
+ const mockStream = (async function* () {
1021
+ yield { type: 'content', value: 'Continue...' };
1022
+ })();
1023
+ mockTurnRunFn.mockReturnValue(mockStream);
1024
+ const mockChat = {
1025
+ addHistory: vi.fn(),
1026
+ getHistory: vi.fn().mockReturnValue([]),
1027
+ };
1028
+ client['chat'] = mockChat;
1029
+ // Use a signal that never gets aborted
1030
+ const abortController = new AbortController();
1031
+ const signal = abortController.signal;
1032
+ // Act - Start the stream with an extremely high turns value
1033
+ // This simulates a case where the turns protection is bypassed
1034
+ const stream = client.sendMessageStream([{ text: 'Start conversation' }], signal, 'prompt-id-3', Number.MAX_SAFE_INTEGER);
1035
+ // Count how many stream events we get
1036
+ let eventCount = 0;
1037
+ const maxTestIterations = 1000; // Higher limit to show the loop continues
1038
+ // Consume the stream and count iterations
1039
+ try {
1040
+ while (true) {
1041
+ const result = await stream.next();
1042
+ if (result.done) {
1043
+ break;
1044
+ }
1045
+ eventCount++;
1046
+ // This test should hit this limit, demonstrating the infinite loop
1047
+ if (eventCount > maxTestIterations) {
1048
+ abortController.abort();
1049
+ // This is the expected behavior - we hit the infinite loop
1050
+ break;
1051
+ }
1052
+ }
1053
+ }
1054
+ catch (error) {
1055
+ // If the test framework times out, that also demonstrates the infinite loop
1056
+ console.error('Test timed out or errored:', error);
1057
+ }
1058
+ // Assert that the fix works - the loop should stop at MAX_TURNS
1059
+ const callCount = mockCheckNextSpeaker.mock.calls.length;
1060
+ // With the fix: even when turns is set to a very high value,
1061
+ // the loop should stop at MAX_TURNS (100)
1062
+ expect(callCount).toBeLessThanOrEqual(100); // Should not exceed MAX_TURNS
1063
+ expect(eventCount).toBeLessThanOrEqual(200); // Should have reasonable number of events
1064
+ console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
1065
+ `${eventCount} events generated (properly bounded by MAX_TURNS)`);
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
+ });
1152
+ describe('Editor context delta', () => {
1153
+ const mockStream = (async function* () {
1154
+ yield { type: 'content', value: 'Hello' };
1155
+ })();
1156
+ beforeEach(() => {
1157
+ client['forceFullIdeContext'] = false; // Reset before each delta test
1158
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1159
+ originalTokenCount: 0,
1160
+ newTokenCount: 0,
1161
+ compressionStatus: CompressionStatus.COMPRESSED,
1162
+ });
1163
+ vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1164
+ mockTurnRunFn.mockReturnValue(mockStream);
1165
+ const mockChat = {
1166
+ addHistory: vi.fn(),
1167
+ setHistory: vi.fn(),
1168
+ // Assume history is not empty for delta checks
1169
+ getHistory: vi
1170
+ .fn()
1171
+ .mockReturnValue([
1172
+ { role: 'user', parts: [{ text: 'previous message' }] },
1173
+ ]),
1174
+ };
1175
+ client['chat'] = mockChat;
1176
+ });
1177
+ const testCases = [
1178
+ {
1179
+ description: 'sends delta when active file changes',
1180
+ previousActiveFile: {
1181
+ path: '/path/to/old/file.ts',
1182
+ cursor: { line: 5, character: 10 },
1183
+ selectedText: 'hello',
1184
+ },
1185
+ currentActiveFile: {
1186
+ path: '/path/to/active/file.ts',
1187
+ cursor: { line: 5, character: 10 },
1188
+ selectedText: 'hello',
1189
+ },
1190
+ shouldSendContext: true,
1191
+ },
1192
+ {
1193
+ description: 'sends delta when cursor line changes',
1194
+ previousActiveFile: {
1195
+ path: '/path/to/active/file.ts',
1196
+ cursor: { line: 1, character: 10 },
1197
+ selectedText: 'hello',
1198
+ },
1199
+ currentActiveFile: {
1200
+ path: '/path/to/active/file.ts',
1201
+ cursor: { line: 5, character: 10 },
1202
+ selectedText: 'hello',
1203
+ },
1204
+ shouldSendContext: true,
1205
+ },
1206
+ {
1207
+ description: 'sends delta when cursor character changes',
1208
+ previousActiveFile: {
1209
+ path: '/path/to/active/file.ts',
1210
+ cursor: { line: 5, character: 1 },
1211
+ selectedText: 'hello',
1212
+ },
1213
+ currentActiveFile: {
1214
+ path: '/path/to/active/file.ts',
1215
+ cursor: { line: 5, character: 10 },
1216
+ selectedText: 'hello',
1217
+ },
1218
+ shouldSendContext: true,
1219
+ },
1220
+ {
1221
+ description: 'sends delta when selected text changes',
1222
+ previousActiveFile: {
1223
+ path: '/path/to/active/file.ts',
1224
+ cursor: { line: 5, character: 10 },
1225
+ selectedText: 'world',
1226
+ },
1227
+ currentActiveFile: {
1228
+ path: '/path/to/active/file.ts',
1229
+ cursor: { line: 5, character: 10 },
1230
+ selectedText: 'hello',
1231
+ },
1232
+ shouldSendContext: true,
1233
+ },
1234
+ {
1235
+ description: 'sends delta when selected text is added',
1236
+ previousActiveFile: {
1237
+ path: '/path/to/active/file.ts',
1238
+ cursor: { line: 5, character: 10 },
1239
+ },
1240
+ currentActiveFile: {
1241
+ path: '/path/to/active/file.ts',
1242
+ cursor: { line: 5, character: 10 },
1243
+ selectedText: 'hello',
1244
+ },
1245
+ shouldSendContext: true,
1246
+ },
1247
+ {
1248
+ description: 'sends delta when selected text is removed',
1249
+ previousActiveFile: {
1250
+ path: '/path/to/active/file.ts',
1251
+ cursor: { line: 5, character: 10 },
1252
+ selectedText: 'hello',
1253
+ },
1254
+ currentActiveFile: {
1255
+ path: '/path/to/active/file.ts',
1256
+ cursor: { line: 5, character: 10 },
1257
+ },
1258
+ shouldSendContext: true,
1259
+ },
1260
+ {
1261
+ description: 'does not send context when nothing changes',
1262
+ previousActiveFile: {
1263
+ path: '/path/to/active/file.ts',
1264
+ cursor: { line: 5, character: 10 },
1265
+ selectedText: 'hello',
1266
+ },
1267
+ currentActiveFile: {
1268
+ path: '/path/to/active/file.ts',
1269
+ cursor: { line: 5, character: 10 },
1270
+ selectedText: 'hello',
1271
+ },
1272
+ shouldSendContext: false,
1273
+ },
1274
+ ];
1275
+ it.each(testCases)('$description', async ({ previousActiveFile, currentActiveFile, shouldSendContext, }) => {
1276
+ // Setup previous context
1277
+ client['lastSentIdeContext'] = {
1278
+ workspaceState: {
1279
+ openFiles: [
1280
+ {
1281
+ path: previousActiveFile.path,
1282
+ cursor: previousActiveFile.cursor,
1283
+ selectedText: previousActiveFile.selectedText,
1284
+ isActive: true,
1285
+ timestamp: Date.now() - 1000,
1286
+ },
1287
+ ],
1288
+ },
1289
+ };
1290
+ // Setup current context
1291
+ vi.mocked(ideContextStore.get).mockReturnValue({
1292
+ workspaceState: {
1293
+ openFiles: [
1294
+ { ...currentActiveFile, isActive: true, timestamp: Date.now() },
1295
+ ],
1296
+ },
1297
+ });
1298
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-delta');
1299
+ for await (const _ of stream) {
1300
+ // consume stream
1301
+ }
1302
+ const mockChat = client['chat'];
1303
+ if (shouldSendContext) {
1304
+ expect(mockChat.addHistory).toHaveBeenCalledWith(expect.objectContaining({
1305
+ parts: expect.arrayContaining([
1306
+ expect.objectContaining({
1307
+ text: expect.stringContaining("Here is a summary of changes in the user's editor context"),
1308
+ }),
1309
+ ]),
1310
+ }));
1311
+ }
1312
+ else {
1313
+ expect(mockChat.addHistory).not.toHaveBeenCalled();
1314
+ }
1315
+ });
1316
+ it('sends full context when history is cleared, even if editor state is unchanged', async () => {
1317
+ const activeFile = {
1318
+ path: '/path/to/active/file.ts',
1319
+ cursor: { line: 5, character: 10 },
1320
+ selectedText: 'hello',
1321
+ };
1322
+ // Setup previous context
1323
+ client['lastSentIdeContext'] = {
1324
+ workspaceState: {
1325
+ openFiles: [
1326
+ {
1327
+ path: activeFile.path,
1328
+ cursor: activeFile.cursor,
1329
+ selectedText: activeFile.selectedText,
1330
+ isActive: true,
1331
+ timestamp: Date.now() - 1000,
1332
+ },
1333
+ ],
1334
+ },
1335
+ };
1336
+ // Setup current context (same as previous)
1337
+ vi.mocked(ideContextStore.get).mockReturnValue({
1338
+ workspaceState: {
1339
+ openFiles: [
1340
+ { ...activeFile, isActive: true, timestamp: Date.now() },
1341
+ ],
1342
+ },
1343
+ });
1344
+ // Make history empty
1345
+ const mockChat = client['chat'];
1346
+ mockChat.getHistory.mockReturnValue([]);
1347
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-history-cleared');
1348
+ for await (const _ of stream) {
1349
+ // consume stream
1350
+ }
1351
+ expect(mockChat.addHistory).toHaveBeenCalledWith(expect.objectContaining({
1352
+ parts: expect.arrayContaining([
1353
+ expect.objectContaining({
1354
+ text: expect.stringContaining("Here is the user's editor context"),
1355
+ }),
1356
+ ]),
1357
+ }));
1358
+ // Also verify it's the full context, not a delta.
1359
+ const call = mockChat.addHistory.mock.calls[0][0];
1360
+ const contextText = call.parts[0].text;
1361
+ const contextJson = JSON.parse(contextText.match(/```json\n(.*)\n```/s)[1]);
1362
+ expect(contextJson).toHaveProperty('activeFile');
1363
+ expect(contextJson.activeFile.path).toBe('/path/to/active/file.ts');
1364
+ });
1365
+ });
1366
+ describe('IDE context with pending tool calls', () => {
1367
+ let mockChat;
1368
+ beforeEach(() => {
1369
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1370
+ originalTokenCount: 0,
1371
+ newTokenCount: 0,
1372
+ compressionStatus: CompressionStatus.COMPRESSED,
1373
+ });
1374
+ const mockStream = (async function* () {
1375
+ yield { type: 'content', value: 'response' };
1376
+ })();
1377
+ mockTurnRunFn.mockReturnValue(mockStream);
1378
+ mockChat = {
1379
+ addHistory: vi.fn(),
1380
+ getHistory: vi.fn().mockReturnValue([]), // Default empty history
1381
+ setHistory: vi.fn(),
1382
+ };
1383
+ client['chat'] = mockChat;
1384
+ vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1385
+ vi.mocked(ideContextStore.get).mockReturnValue({
1386
+ workspaceState: {
1387
+ openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
1388
+ },
1389
+ });
1390
+ });
1391
+ it('should NOT add IDE context when a tool call is pending', async () => {
1392
+ // Arrange: History ends with a functionCall from the model
1393
+ const historyWithPendingCall = [
1394
+ { role: 'user', parts: [{ text: 'Please use a tool.' }] },
1395
+ {
1396
+ role: 'model',
1397
+ parts: [{ functionCall: { name: 'some_tool', args: {} } }],
1398
+ },
1399
+ ];
1400
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyWithPendingCall);
1401
+ // Act: Simulate sending the tool's response back
1402
+ const stream = client.sendMessageStream([
1403
+ {
1404
+ functionResponse: {
1405
+ name: 'some_tool',
1406
+ response: { success: true },
1407
+ },
1408
+ },
1409
+ ], new AbortController().signal, 'prompt-id-tool-response');
1410
+ for await (const _ of stream) {
1411
+ // consume stream to complete the call
1412
+ }
1413
+ // Assert: The IDE context message should NOT have been added to the history.
1414
+ expect(mockChat.addHistory).not.toHaveBeenCalledWith(expect.objectContaining({
1415
+ parts: expect.arrayContaining([
1416
+ expect.objectContaining({
1417
+ text: expect.stringContaining("user's editor context"),
1418
+ }),
1419
+ ]),
1420
+ }));
1421
+ });
1422
+ it('should add IDE context when no tool call is pending', async () => {
1423
+ // Arrange: History is normal, no pending calls
1424
+ const normalHistory = [
1425
+ { role: 'user', parts: [{ text: 'A normal message.' }] },
1426
+ { role: 'model', parts: [{ text: 'A normal response.' }] },
1427
+ ];
1428
+ vi.mocked(mockChat.getHistory).mockReturnValue(normalHistory);
1429
+ // Act
1430
+ const stream = client.sendMessageStream([{ text: 'Another normal message' }], new AbortController().signal, 'prompt-id-normal');
1431
+ for await (const _ of stream) {
1432
+ // consume stream
1433
+ }
1434
+ // Assert: The IDE context message SHOULD have been added.
1435
+ expect(mockChat.addHistory).toHaveBeenCalledWith(expect.objectContaining({
1436
+ role: 'user',
1437
+ parts: expect.arrayContaining([
1438
+ expect.objectContaining({
1439
+ text: expect.stringContaining("user's editor context"),
1440
+ }),
1441
+ ]),
1442
+ }));
1443
+ });
1444
+ it('should send the latest IDE context on the next message after a skipped context', async () => {
1445
+ // --- Step 1: A tool call is pending, context should be skipped ---
1446
+ // Arrange: History ends with a functionCall
1447
+ const historyWithPendingCall = [
1448
+ { role: 'user', parts: [{ text: 'Please use a tool.' }] },
1449
+ {
1450
+ role: 'model',
1451
+ parts: [{ functionCall: { name: 'some_tool', args: {} } }],
1452
+ },
1453
+ ];
1454
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyWithPendingCall);
1455
+ // Arrange: Set the initial IDE context
1456
+ const initialIdeContext = {
1457
+ workspaceState: {
1458
+ openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
1459
+ },
1460
+ };
1461
+ vi.mocked(ideContextStore.get).mockReturnValue(initialIdeContext);
1462
+ // Act: Send the tool response
1463
+ let stream = client.sendMessageStream([
1464
+ {
1465
+ functionResponse: {
1466
+ name: 'some_tool',
1467
+ response: { success: true },
1468
+ },
1469
+ },
1470
+ ], new AbortController().signal, 'prompt-id-tool-response');
1471
+ for await (const _ of stream) {
1472
+ /* consume */
1473
+ }
1474
+ // Assert: The initial context was NOT sent
1475
+ expect(mockChat.addHistory).not.toHaveBeenCalledWith(expect.objectContaining({
1476
+ parts: expect.arrayContaining([
1477
+ expect.objectContaining({
1478
+ text: expect.stringContaining("user's editor context"),
1479
+ }),
1480
+ ]),
1481
+ }));
1482
+ // --- Step 2: A new message is sent, latest context should be included ---
1483
+ // Arrange: The model has responded to the tool, and the user is sending a new message.
1484
+ const historyAfterToolResponse = [
1485
+ ...historyWithPendingCall,
1486
+ {
1487
+ role: 'user',
1488
+ parts: [
1489
+ {
1490
+ functionResponse: {
1491
+ name: 'some_tool',
1492
+ response: { success: true },
1493
+ },
1494
+ },
1495
+ ],
1496
+ },
1497
+ { role: 'model', parts: [{ text: 'The tool ran successfully.' }] },
1498
+ ];
1499
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyAfterToolResponse);
1500
+ vi.mocked(mockChat.addHistory).mockClear(); // Clear previous calls for the next assertion
1501
+ // Arrange: The IDE context has now changed
1502
+ const newIdeContext = {
1503
+ workspaceState: {
1504
+ openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
1505
+ },
1506
+ };
1507
+ vi.mocked(ideContextStore.get).mockReturnValue(newIdeContext);
1508
+ // Act: Send a new, regular user message
1509
+ stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1510
+ for await (const _ of stream) {
1511
+ /* consume */
1512
+ }
1513
+ // Assert: The NEW context was sent as a FULL context because there was no previously sent context.
1514
+ const addHistoryCalls = vi.mocked(mockChat.addHistory).mock.calls;
1515
+ const contextCall = addHistoryCalls.find((call) => JSON.stringify(call[0]).includes("user's editor context"));
1516
+ expect(contextCall).toBeDefined();
1517
+ expect(JSON.stringify(contextCall[0])).toContain("Here is the user's editor context as a JSON object");
1518
+ // Check that the sent context is the new one (fileB.ts)
1519
+ expect(JSON.stringify(contextCall[0])).toContain('fileB.ts');
1520
+ // Check that the sent context is NOT the old one (fileA.ts)
1521
+ expect(JSON.stringify(contextCall[0])).not.toContain('fileA.ts');
1522
+ });
1523
+ it('should send a context DELTA on the next message after a skipped context', async () => {
1524
+ // --- Step 0: Establish an initial context ---
1525
+ vi.mocked(mockChat.getHistory).mockReturnValue([]); // Start with empty history
1526
+ const contextA = {
1527
+ workspaceState: {
1528
+ openFiles: [
1529
+ {
1530
+ path: '/path/to/fileA.ts',
1531
+ isActive: true,
1532
+ timestamp: Date.now(),
1533
+ },
1534
+ ],
1535
+ },
1536
+ };
1537
+ vi.mocked(ideContextStore.get).mockReturnValue(contextA);
1538
+ // Act: Send a regular message to establish the initial context
1539
+ let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
1540
+ for await (const _ of stream) {
1541
+ /* consume */
1542
+ }
1543
+ // Assert: Full context for fileA.ts was sent and stored.
1544
+ const initialCall = vi.mocked(mockChat.addHistory).mock.calls[0][0];
1545
+ expect(JSON.stringify(initialCall)).toContain("user's editor context as a JSON object");
1546
+ expect(JSON.stringify(initialCall)).toContain('fileA.ts');
1547
+ // This implicitly tests that `lastSentIdeContext` is now set internally by the client.
1548
+ vi.mocked(mockChat.addHistory).mockClear();
1549
+ // --- Step 1: A tool call is pending, context should be skipped ---
1550
+ const historyWithPendingCall = [
1551
+ { role: 'user', parts: [{ text: 'Please use a tool.' }] },
1552
+ {
1553
+ role: 'model',
1554
+ parts: [{ functionCall: { name: 'some_tool', args: {} } }],
1555
+ },
1556
+ ];
1557
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyWithPendingCall);
1558
+ // Arrange: IDE context changes, but this should be skipped
1559
+ const contextB = {
1560
+ workspaceState: {
1561
+ openFiles: [
1562
+ {
1563
+ path: '/path/to/fileB.ts',
1564
+ isActive: true,
1565
+ timestamp: Date.now(),
1566
+ },
1567
+ ],
1568
+ },
1569
+ };
1570
+ vi.mocked(ideContextStore.get).mockReturnValue(contextB);
1571
+ // Act: Send the tool response
1572
+ stream = client.sendMessageStream([
1573
+ {
1574
+ functionResponse: {
1575
+ name: 'some_tool',
1576
+ response: { success: true },
1577
+ },
1578
+ },
1579
+ ], new AbortController().signal, 'prompt-id-tool-response');
1580
+ for await (const _ of stream) {
1581
+ /* consume */
1582
+ }
1583
+ // Assert: No context was sent
1584
+ expect(mockChat.addHistory).not.toHaveBeenCalled();
1585
+ // --- Step 2: A new message is sent, latest context DELTA should be included ---
1586
+ const historyAfterToolResponse = [
1587
+ ...historyWithPendingCall,
1588
+ {
1589
+ role: 'user',
1590
+ parts: [
1591
+ {
1592
+ functionResponse: {
1593
+ name: 'some_tool',
1594
+ response: { success: true },
1595
+ },
1596
+ },
1597
+ ],
1598
+ },
1599
+ { role: 'model', parts: [{ text: 'The tool ran successfully.' }] },
1600
+ ];
1601
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyAfterToolResponse);
1602
+ // Arrange: The IDE context has changed again
1603
+ const contextC = {
1604
+ workspaceState: {
1605
+ openFiles: [
1606
+ // fileA is now closed, fileC is open
1607
+ {
1608
+ path: '/path/to/fileC.ts',
1609
+ isActive: true,
1610
+ timestamp: Date.now(),
1611
+ },
1612
+ ],
1613
+ },
1614
+ };
1615
+ vi.mocked(ideContextStore.get).mockReturnValue(contextC);
1616
+ // Act: Send a new, regular user message
1617
+ stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1618
+ for await (const _ of stream) {
1619
+ /* consume */
1620
+ }
1621
+ // Assert: The DELTA context was sent
1622
+ const finalCall = vi.mocked(mockChat.addHistory).mock.calls[0][0];
1623
+ expect(JSON.stringify(finalCall)).toContain('summary of changes');
1624
+ // The delta should reflect fileA being closed and fileC being opened.
1625
+ expect(JSON.stringify(finalCall)).toContain('filesClosed');
1626
+ expect(JSON.stringify(finalCall)).toContain('fileA.ts');
1627
+ expect(JSON.stringify(finalCall)).toContain('activeFileChanged');
1628
+ expect(JSON.stringify(finalCall)).toContain('fileC.ts');
1629
+ });
1630
+ });
1631
+ it('should not call checkNextSpeaker when turn.run() yields an error', async () => {
1632
+ // Arrange
1633
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
1634
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
1635
+ const mockStream = (async function* () {
1636
+ yield {
1637
+ type: GeminiEventType.Error,
1638
+ value: { error: { message: 'test error' } },
1639
+ };
1640
+ })();
1641
+ mockTurnRunFn.mockReturnValue(mockStream);
1642
+ const mockChat = {
1643
+ addHistory: vi.fn(),
1644
+ getHistory: vi.fn().mockReturnValue([]),
1645
+ };
1646
+ client['chat'] = mockChat;
1647
+ // Act
1648
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1649
+ for await (const _ of stream) {
1650
+ // consume stream
1651
+ }
1652
+ // Assert
1653
+ expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
1654
+ });
1655
+ it('should not call checkNextSpeaker when turn.run() yields a value then an error', async () => {
1656
+ // Arrange
1657
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
1658
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
1659
+ const mockStream = (async function* () {
1660
+ yield { type: GeminiEventType.Content, value: 'some content' };
1661
+ yield {
1662
+ type: GeminiEventType.Error,
1663
+ value: { error: { message: 'test error' } },
1664
+ };
1665
+ })();
1666
+ mockTurnRunFn.mockReturnValue(mockStream);
1667
+ const mockChat = {
1668
+ addHistory: vi.fn(),
1669
+ getHistory: vi.fn().mockReturnValue([]),
1670
+ };
1671
+ client['chat'] = mockChat;
1672
+ // Act
1673
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1674
+ for await (const _ of stream) {
1675
+ // consume stream
1676
+ }
1677
+ // Assert
1678
+ expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
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
+ });
1709
+ });
1710
+ describe('generateContent', () => {
1711
+ it('should call generateContent with the correct parameters', async () => {
1712
+ const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
1713
+ const generationConfig = { temperature: 0.5 };
1714
+ const abortSignal = new AbortController().signal;
1715
+ await client.generateContent(contents, generationConfig, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
1716
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
1717
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1718
+ config: {
1719
+ abortSignal,
1720
+ systemInstruction: getCoreSystemPrompt(''),
1721
+ temperature: 0.5,
1722
+ topP: 1,
1723
+ },
1724
+ contents,
1725
+ }, 'test-session-id');
1726
+ });
1727
+ it('should use current model from config for content generation', async () => {
1728
+ const initialModel = client['config'].getModel();
1729
+ const contents = [{ role: 'user', parts: [{ text: 'test' }] }];
1730
+ const currentModel = initialModel + '-changed';
1731
+ vi.spyOn(client['config'], 'getModel').mockReturnValueOnce(currentModel);
1732
+ await client.generateContent(contents, {}, new AbortController().signal, DEFAULT_GEMINI_FLASH_MODEL);
1733
+ expect(mockContentGenerator.generateContent).not.toHaveBeenCalledWith({
1734
+ model: initialModel,
1735
+ config: expect.any(Object),
1736
+ contents,
1737
+ });
1738
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
1739
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1740
+ config: expect.any(Object),
1741
+ contents,
1742
+ }, 'test-session-id');
1743
+ });
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');
1755
+ });
1756
+ });
1757
+ });
1758
+ //# sourceMappingURL=client.test.js.map