@blackbox_ai/blackbox-cli-core 0.0.4

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 (897) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +420 -0
  3. package/dist/.last_build +0 -0
  4. package/dist/index.d.ts +13 -0
  5. package/dist/index.js +14 -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/blackbox/blackboxContentGenerator.d.ts +70 -0
  11. package/dist/src/blackbox/blackboxContentGenerator.js +180 -0
  12. package/dist/src/blackbox/blackboxContentGenerator.js.map +1 -0
  13. package/dist/src/blackbox/blackboxContentGenerator.test.d.ts +6 -0
  14. package/dist/src/blackbox/blackboxContentGenerator.test.js +1178 -0
  15. package/dist/src/blackbox/blackboxContentGenerator.test.js.map +1 -0
  16. package/dist/src/blackbox/blackboxOAuth2.d.ts +191 -0
  17. package/dist/src/blackbox/blackboxOAuth2.js +566 -0
  18. package/dist/src/blackbox/blackboxOAuth2.js.map +1 -0
  19. package/dist/src/blackbox/blackboxOAuth2.test.d.ts +6 -0
  20. package/dist/src/blackbox/blackboxOAuth2.test.js +1718 -0
  21. package/dist/src/blackbox/blackboxOAuth2.test.js.map +1 -0
  22. package/dist/src/blackbox/sharedTokenManager.d.ts +196 -0
  23. package/dist/src/blackbox/sharedTokenManager.js +647 -0
  24. package/dist/src/blackbox/sharedTokenManager.js.map +1 -0
  25. package/dist/src/blackbox/sharedTokenManager.test.d.ts +7 -0
  26. package/dist/src/blackbox/sharedTokenManager.test.js +662 -0
  27. package/dist/src/blackbox/sharedTokenManager.test.js.map +1 -0
  28. package/dist/src/code_assist/codeAssist.d.ts +10 -0
  29. package/dist/src/code_assist/codeAssist.js +19 -0
  30. package/dist/src/code_assist/codeAssist.js.map +1 -0
  31. package/dist/src/code_assist/converter.d.ts +72 -0
  32. package/dist/src/code_assist/converter.js +159 -0
  33. package/dist/src/code_assist/converter.js.map +1 -0
  34. package/dist/src/code_assist/converter.test.d.ts +6 -0
  35. package/dist/src/code_assist/converter.test.js +362 -0
  36. package/dist/src/code_assist/converter.test.js.map +1 -0
  37. package/dist/src/code_assist/oauth2.d.ts +22 -0
  38. package/dist/src/code_assist/oauth2.js +368 -0
  39. package/dist/src/code_assist/oauth2.js.map +1 -0
  40. package/dist/src/code_assist/oauth2.test.d.ts +6 -0
  41. package/dist/src/code_assist/oauth2.test.js +436 -0
  42. package/dist/src/code_assist/oauth2.test.js.map +1 -0
  43. package/dist/src/code_assist/server.d.ts +37 -0
  44. package/dist/src/code_assist/server.js +125 -0
  45. package/dist/src/code_assist/server.js.map +1 -0
  46. package/dist/src/code_assist/server.test.d.ts +6 -0
  47. package/dist/src/code_assist/server.test.js +134 -0
  48. package/dist/src/code_assist/server.test.js.map +1 -0
  49. package/dist/src/code_assist/setup.d.ts +20 -0
  50. package/dist/src/code_assist/setup.js +101 -0
  51. package/dist/src/code_assist/setup.js.map +1 -0
  52. package/dist/src/code_assist/setup.test.d.ts +6 -0
  53. package/dist/src/code_assist/setup.test.js +171 -0
  54. package/dist/src/code_assist/setup.test.js.map +1 -0
  55. package/dist/src/code_assist/types.d.ts +148 -0
  56. package/dist/src/code_assist/types.js +46 -0
  57. package/dist/src/code_assist/types.js.map +1 -0
  58. package/dist/src/config/blackboxModels.d.ts +28 -0
  59. package/dist/src/config/blackboxModels.js +148 -0
  60. package/dist/src/config/blackboxModels.js.map +1 -0
  61. package/dist/src/config/config.d.ts +381 -0
  62. package/dist/src/config/config.js +753 -0
  63. package/dist/src/config/config.js.map +1 -0
  64. package/dist/src/config/config.test.d.ts +6 -0
  65. package/dist/src/config/config.test.js +674 -0
  66. package/dist/src/config/config.test.js.map +1 -0
  67. package/dist/src/config/flashFallback.test.d.ts +6 -0
  68. package/dist/src/config/flashFallback.test.js +87 -0
  69. package/dist/src/config/flashFallback.test.js.map +1 -0
  70. package/dist/src/config/models.d.ts +12 -0
  71. package/dist/src/config/models.js +13 -0
  72. package/dist/src/config/models.js.map +1 -0
  73. package/dist/src/config/storage.d.ts +32 -0
  74. package/dist/src/config/storage.js +90 -0
  75. package/dist/src/config/storage.js.map +1 -0
  76. package/dist/src/config/storage.test.d.ts +6 -0
  77. package/dist/src/config/storage.test.js +43 -0
  78. package/dist/src/config/storage.test.js.map +1 -0
  79. package/dist/src/core/__tests__/openaiTimeoutHandling.test.d.ts +6 -0
  80. package/dist/src/core/__tests__/openaiTimeoutHandling.test.js +292 -0
  81. package/dist/src/core/__tests__/openaiTimeoutHandling.test.js.map +1 -0
  82. package/dist/src/core/__tests__/orphanedToolCallsTest.d.ts +64 -0
  83. package/dist/src/core/__tests__/orphanedToolCallsTest.js +122 -0
  84. package/dist/src/core/__tests__/orphanedToolCallsTest.js.map +1 -0
  85. package/dist/src/core/client.d.ts +75 -0
  86. package/dist/src/core/client.js +826 -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 +2059 -0
  90. package/dist/src/core/client.test.js.map +1 -0
  91. package/dist/src/core/contentGenerator.d.ts +51 -0
  92. package/dist/src/core/contentGenerator.js +149 -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 +133 -0
  96. package/dist/src/core/contentGenerator.test.js.map +1 -0
  97. package/dist/src/core/coreToolScheduler.d.ts +126 -0
  98. package/dist/src/core/coreToolScheduler.js +630 -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 +1228 -0
  102. package/dist/src/core/coreToolScheduler.test.js.map +1 -0
  103. package/dist/src/core/geminiChat.d.ts +139 -0
  104. package/dist/src/core/geminiChat.js +636 -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 +1178 -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/geminiRequest.test.d.ts +6 -0
  113. package/dist/src/core/geminiRequest.test.js +73 -0
  114. package/dist/src/core/geminiRequest.test.js.map +1 -0
  115. package/dist/src/core/logger.d.ts +68 -0
  116. package/dist/src/core/logger.js +370 -0
  117. package/dist/src/core/logger.js.map +1 -0
  118. package/dist/src/core/logger.test.d.ts +6 -0
  119. package/dist/src/core/logger.test.js +591 -0
  120. package/dist/src/core/logger.test.js.map +1 -0
  121. package/dist/src/core/loggingContentGenerator.d.ts +25 -0
  122. package/dist/src/core/loggingContentGenerator.js +97 -0
  123. package/dist/src/core/loggingContentGenerator.js.map +1 -0
  124. package/dist/src/core/nonInteractiveToolExecutor.d.ts +10 -0
  125. package/dist/src/core/nonInteractiveToolExecutor.js +24 -0
  126. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -0
  127. package/dist/src/core/nonInteractiveToolExecutor.test.d.ts +6 -0
  128. package/dist/src/core/nonInteractiveToolExecutor.test.js +236 -0
  129. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -0
  130. package/dist/src/core/openaiContentGenerator/constants.d.ts +2 -0
  131. package/dist/src/core/openaiContentGenerator/constants.js +3 -0
  132. package/dist/src/core/openaiContentGenerator/constants.js.map +1 -0
  133. package/dist/src/core/openaiContentGenerator/converter.d.ts +107 -0
  134. package/dist/src/core/openaiContentGenerator/converter.js +802 -0
  135. package/dist/src/core/openaiContentGenerator/converter.js.map +1 -0
  136. package/dist/src/core/openaiContentGenerator/converter.test.d.ts +6 -0
  137. package/dist/src/core/openaiContentGenerator/converter.test.js +47 -0
  138. package/dist/src/core/openaiContentGenerator/converter.test.js.map +1 -0
  139. package/dist/src/core/openaiContentGenerator/errorHandler.d.ts +20 -0
  140. package/dist/src/core/openaiContentGenerator/errorHandler.js +82 -0
  141. package/dist/src/core/openaiContentGenerator/errorHandler.js.map +1 -0
  142. package/dist/src/core/openaiContentGenerator/errorHandler.test.d.ts +6 -0
  143. package/dist/src/core/openaiContentGenerator/errorHandler.test.js +287 -0
  144. package/dist/src/core/openaiContentGenerator/errorHandler.test.js.map +1 -0
  145. package/dist/src/core/openaiContentGenerator/index.d.ts +22 -0
  146. package/dist/src/core/openaiContentGenerator/index.js +41 -0
  147. package/dist/src/core/openaiContentGenerator/index.js.map +1 -0
  148. package/dist/src/core/openaiContentGenerator/openaiContentGenerator.d.ts +21 -0
  149. package/dist/src/core/openaiContentGenerator/openaiContentGenerator.js +105 -0
  150. package/dist/src/core/openaiContentGenerator/openaiContentGenerator.js.map +1 -0
  151. package/dist/src/core/openaiContentGenerator/openaiContentGenerator.test.d.ts +6 -0
  152. package/dist/src/core/openaiContentGenerator/openaiContentGenerator.test.js +230 -0
  153. package/dist/src/core/openaiContentGenerator/openaiContentGenerator.test.js.map +1 -0
  154. package/dist/src/core/openaiContentGenerator/pipeline.d.ts +66 -0
  155. package/dist/src/core/openaiContentGenerator/pipeline.js +256 -0
  156. package/dist/src/core/openaiContentGenerator/pipeline.js.map +1 -0
  157. package/dist/src/core/openaiContentGenerator/pipeline.test.d.ts +6 -0
  158. package/dist/src/core/openaiContentGenerator/pipeline.test.js +977 -0
  159. package/dist/src/core/openaiContentGenerator/pipeline.test.js.map +1 -0
  160. package/dist/src/core/openaiContentGenerator/provider/README.md +61 -0
  161. package/dist/src/core/openaiContentGenerator/provider/dashscope.d.ts +63 -0
  162. package/dist/src/core/openaiContentGenerator/provider/dashscope.js +241 -0
  163. package/dist/src/core/openaiContentGenerator/provider/dashscope.js.map +1 -0
  164. package/dist/src/core/openaiContentGenerator/provider/dashscope.test.d.ts +6 -0
  165. package/dist/src/core/openaiContentGenerator/provider/dashscope.test.js +707 -0
  166. package/dist/src/core/openaiContentGenerator/provider/dashscope.test.js.map +1 -0
  167. package/dist/src/core/openaiContentGenerator/provider/deepseek.d.ts +14 -0
  168. package/dist/src/core/openaiContentGenerator/provider/deepseek.js +52 -0
  169. package/dist/src/core/openaiContentGenerator/provider/deepseek.js.map +1 -0
  170. package/dist/src/core/openaiContentGenerator/provider/deepseek.test.d.ts +6 -0
  171. package/dist/src/core/openaiContentGenerator/provider/deepseek.test.js +103 -0
  172. package/dist/src/core/openaiContentGenerator/provider/deepseek.test.js.map +1 -0
  173. package/dist/src/core/openaiContentGenerator/provider/default.d.ts +15 -0
  174. package/dist/src/core/openaiContentGenerator/provider/default.js +38 -0
  175. package/dist/src/core/openaiContentGenerator/provider/default.js.map +1 -0
  176. package/dist/src/core/openaiContentGenerator/provider/default.test.d.ts +6 -0
  177. package/dist/src/core/openaiContentGenerator/provider/default.test.js +176 -0
  178. package/dist/src/core/openaiContentGenerator/provider/default.test.js.map +1 -0
  179. package/dist/src/core/openaiContentGenerator/provider/index.d.ts +5 -0
  180. package/dist/src/core/openaiContentGenerator/provider/index.js +5 -0
  181. package/dist/src/core/openaiContentGenerator/provider/index.js.map +1 -0
  182. package/dist/src/core/openaiContentGenerator/provider/openrouter.d.ts +8 -0
  183. package/dist/src/core/openaiContentGenerator/provider/openrouter.js +21 -0
  184. package/dist/src/core/openaiContentGenerator/provider/openrouter.js.map +1 -0
  185. package/dist/src/core/openaiContentGenerator/provider/openrouter.test.d.ts +6 -0
  186. package/dist/src/core/openaiContentGenerator/provider/openrouter.test.js +152 -0
  187. package/dist/src/core/openaiContentGenerator/provider/openrouter.test.js.map +1 -0
  188. package/dist/src/core/openaiContentGenerator/provider/types.d.ts +23 -0
  189. package/dist/src/core/openaiContentGenerator/provider/types.js +2 -0
  190. package/dist/src/core/openaiContentGenerator/provider/types.js.map +1 -0
  191. package/dist/src/core/openaiContentGenerator/streamingToolCallParser.d.ts +142 -0
  192. package/dist/src/core/openaiContentGenerator/streamingToolCallParser.js +363 -0
  193. package/dist/src/core/openaiContentGenerator/streamingToolCallParser.js.map +1 -0
  194. package/dist/src/core/openaiContentGenerator/streamingToolCallParser.test.d.ts +6 -0
  195. package/dist/src/core/openaiContentGenerator/streamingToolCallParser.test.js +537 -0
  196. package/dist/src/core/openaiContentGenerator/streamingToolCallParser.test.js.map +1 -0
  197. package/dist/src/core/openaiContentGenerator/telemetryService.d.ts +35 -0
  198. package/dist/src/core/openaiContentGenerator/telemetryService.js +146 -0
  199. package/dist/src/core/openaiContentGenerator/telemetryService.js.map +1 -0
  200. package/dist/src/core/openaiContentGenerator/telemetryService.test.d.ts +6 -0
  201. package/dist/src/core/openaiContentGenerator/telemetryService.test.js +978 -0
  202. package/dist/src/core/openaiContentGenerator/telemetryService.test.js.map +1 -0
  203. package/dist/src/core/prompts.d.ts +76 -0
  204. package/dist/src/core/prompts.js +860 -0
  205. package/dist/src/core/prompts.js.map +1 -0
  206. package/dist/src/core/prompts.test.d.ts +6 -0
  207. package/dist/src/core/prompts.test.js +436 -0
  208. package/dist/src/core/prompts.test.js.map +1 -0
  209. package/dist/src/core/tokenLimits.d.ts +25 -0
  210. package/dist/src/core/tokenLimits.js +175 -0
  211. package/dist/src/core/tokenLimits.js.map +1 -0
  212. package/dist/src/core/tokenLimits.test.d.ts +1 -0
  213. package/dist/src/core/tokenLimits.test.js +261 -0
  214. package/dist/src/core/tokenLimits.test.js.map +1 -0
  215. package/dist/src/core/turn.d.ts +140 -0
  216. package/dist/src/core/turn.js +168 -0
  217. package/dist/src/core/turn.js.map +1 -0
  218. package/dist/src/core/turn.test.d.ts +6 -0
  219. package/dist/src/core/turn.test.js +450 -0
  220. package/dist/src/core/turn.test.js.map +1 -0
  221. package/dist/src/generated/git-commit.d.ts +7 -0
  222. package/dist/src/generated/git-commit.js +10 -0
  223. package/dist/src/generated/git-commit.js.map +1 -0
  224. package/dist/src/ide/constants.d.ts +6 -0
  225. package/dist/src/ide/constants.js +7 -0
  226. package/dist/src/ide/constants.js.map +1 -0
  227. package/dist/src/ide/detect-ide.d.ts +25 -0
  228. package/dist/src/ide/detect-ide.js +104 -0
  229. package/dist/src/ide/detect-ide.js.map +1 -0
  230. package/dist/src/ide/detect-ide.test.d.ts +6 -0
  231. package/dist/src/ide/detect-ide.test.js +120 -0
  232. package/dist/src/ide/detect-ide.test.js.map +1 -0
  233. package/dist/src/ide/ide-client.d.ts +67 -0
  234. package/dist/src/ide/ide-client.js +424 -0
  235. package/dist/src/ide/ide-client.js.map +1 -0
  236. package/dist/src/ide/ide-client.test.d.ts +6 -0
  237. package/dist/src/ide/ide-client.test.js +165 -0
  238. package/dist/src/ide/ide-client.test.js.map +1 -0
  239. package/dist/src/ide/ide-installer.d.ts +14 -0
  240. package/dist/src/ide/ide-installer.js +107 -0
  241. package/dist/src/ide/ide-installer.js.map +1 -0
  242. package/dist/src/ide/ide-installer.test.d.ts +6 -0
  243. package/dist/src/ide/ide-installer.test.js +113 -0
  244. package/dist/src/ide/ide-installer.test.js.map +1 -0
  245. package/dist/src/ide/ideContext.d.ts +374 -0
  246. package/dist/src/ide/ideContext.js +147 -0
  247. package/dist/src/ide/ideContext.js.map +1 -0
  248. package/dist/src/ide/ideContext.test.d.ts +6 -0
  249. package/dist/src/ide/ideContext.test.js +265 -0
  250. package/dist/src/ide/ideContext.test.js.map +1 -0
  251. package/dist/src/ide/process-utils.d.ts +21 -0
  252. package/dist/src/ide/process-utils.js +164 -0
  253. package/dist/src/ide/process-utils.js.map +1 -0
  254. package/dist/src/ide/process-utils.test.d.ts +6 -0
  255. package/dist/src/ide/process-utils.test.js +158 -0
  256. package/dist/src/ide/process-utils.test.js.map +1 -0
  257. package/dist/src/index.d.ts +86 -0
  258. package/dist/src/index.js +97 -0
  259. package/dist/src/index.js.map +1 -0
  260. package/dist/src/index.test.d.ts +6 -0
  261. package/dist/src/index.test.js +12 -0
  262. package/dist/src/index.test.js.map +1 -0
  263. package/dist/src/mcp/google-auth-provider.d.ts +23 -0
  264. package/dist/src/mcp/google-auth-provider.js +72 -0
  265. package/dist/src/mcp/google-auth-provider.js.map +1 -0
  266. package/dist/src/mcp/google-auth-provider.test.d.ts +6 -0
  267. package/dist/src/mcp/google-auth-provider.test.js +89 -0
  268. package/dist/src/mcp/google-auth-provider.test.js.map +1 -0
  269. package/dist/src/mcp/oauth-provider.d.ts +146 -0
  270. package/dist/src/mcp/oauth-provider.js +601 -0
  271. package/dist/src/mcp/oauth-provider.js.map +1 -0
  272. package/dist/src/mcp/oauth-provider.test.d.ts +6 -0
  273. package/dist/src/mcp/oauth-provider.test.js +672 -0
  274. package/dist/src/mcp/oauth-provider.test.js.map +1 -0
  275. package/dist/src/mcp/oauth-token-storage.d.ts +61 -0
  276. package/dist/src/mcp/oauth-token-storage.js +148 -0
  277. package/dist/src/mcp/oauth-token-storage.js.map +1 -0
  278. package/dist/src/mcp/oauth-token-storage.test.d.ts +6 -0
  279. package/dist/src/mcp/oauth-token-storage.test.js +208 -0
  280. package/dist/src/mcp/oauth-token-storage.test.js.map +1 -0
  281. package/dist/src/mcp/oauth-utils.d.ts +119 -0
  282. package/dist/src/mcp/oauth-utils.js +235 -0
  283. package/dist/src/mcp/oauth-utils.js.map +1 -0
  284. package/dist/src/mcp/oauth-utils.test.d.ts +6 -0
  285. package/dist/src/mcp/oauth-utils.test.js +199 -0
  286. package/dist/src/mcp/oauth-utils.test.js.map +1 -0
  287. package/dist/src/mcp/token-storage/base-token-storage.d.ts +19 -0
  288. package/dist/src/mcp/token-storage/base-token-storage.js +36 -0
  289. package/dist/src/mcp/token-storage/base-token-storage.js.map +1 -0
  290. package/dist/src/mcp/token-storage/base-token-storage.test.d.ts +6 -0
  291. package/dist/src/mcp/token-storage/base-token-storage.test.js +160 -0
  292. package/dist/src/mcp/token-storage/base-token-storage.test.js.map +1 -0
  293. package/dist/src/mcp/token-storage/types.d.ts +34 -0
  294. package/dist/src/mcp/token-storage/types.js +7 -0
  295. package/dist/src/mcp/token-storage/types.js.map +1 -0
  296. package/dist/src/mocks/msw.d.ts +6 -0
  297. package/dist/src/mocks/msw.js +8 -0
  298. package/dist/src/mocks/msw.js.map +1 -0
  299. package/dist/src/prompts/mcp-prompts.d.ts +8 -0
  300. package/dist/src/prompts/mcp-prompts.js +13 -0
  301. package/dist/src/prompts/mcp-prompts.js.map +1 -0
  302. package/dist/src/prompts/prompt-registry.d.ts +34 -0
  303. package/dist/src/prompts/prompt-registry.js +63 -0
  304. package/dist/src/prompts/prompt-registry.js.map +1 -0
  305. package/dist/src/services/chatRecordingService.d.ts +150 -0
  306. package/dist/src/services/chatRecordingService.js +321 -0
  307. package/dist/src/services/chatRecordingService.js.map +1 -0
  308. package/dist/src/services/chatRecordingService.test.d.ts +6 -0
  309. package/dist/src/services/chatRecordingService.test.js +290 -0
  310. package/dist/src/services/chatRecordingService.test.js.map +1 -0
  311. package/dist/src/services/fileDiscoveryService.d.ts +35 -0
  312. package/dist/src/services/fileDiscoveryService.js +91 -0
  313. package/dist/src/services/fileDiscoveryService.js.map +1 -0
  314. package/dist/src/services/fileDiscoveryService.test.d.ts +6 -0
  315. package/dist/src/services/fileDiscoveryService.test.js +143 -0
  316. package/dist/src/services/fileDiscoveryService.test.js.map +1 -0
  317. package/dist/src/services/fileSystemService.d.ts +31 -0
  318. package/dist/src/services/fileSystemService.js +18 -0
  319. package/dist/src/services/fileSystemService.js.map +1 -0
  320. package/dist/src/services/fileSystemService.test.d.ts +6 -0
  321. package/dist/src/services/fileSystemService.test.js +41 -0
  322. package/dist/src/services/fileSystemService.test.js.map +1 -0
  323. package/dist/src/services/gitService.d.ts +23 -0
  324. package/dist/src/services/gitService.js +110 -0
  325. package/dist/src/services/gitService.js.map +1 -0
  326. package/dist/src/services/gitService.test.d.ts +6 -0
  327. package/dist/src/services/gitService.test.js +212 -0
  328. package/dist/src/services/gitService.test.js.map +1 -0
  329. package/dist/src/services/loopDetectionService.d.ts +98 -0
  330. package/dist/src/services/loopDetectionService.js +363 -0
  331. package/dist/src/services/loopDetectionService.js.map +1 -0
  332. package/dist/src/services/loopDetectionService.test.d.ts +6 -0
  333. package/dist/src/services/loopDetectionService.test.js +560 -0
  334. package/dist/src/services/loopDetectionService.test.js.map +1 -0
  335. package/dist/src/services/shellExecutionService.d.ts +68 -0
  336. package/dist/src/services/shellExecutionService.js +332 -0
  337. package/dist/src/services/shellExecutionService.js.map +1 -0
  338. package/dist/src/services/shellExecutionService.test.d.ts +6 -0
  339. package/dist/src/services/shellExecutionService.test.js +517 -0
  340. package/dist/src/services/shellExecutionService.test.js.map +1 -0
  341. package/dist/src/subagents/builtin-agents.d.ts +35 -0
  342. package/dist/src/subagents/builtin-agents.js +85 -0
  343. package/dist/src/subagents/builtin-agents.js.map +1 -0
  344. package/dist/src/subagents/builtin-agents.test.d.ts +6 -0
  345. package/dist/src/subagents/builtin-agents.test.js +78 -0
  346. package/dist/src/subagents/builtin-agents.test.js.map +1 -0
  347. package/dist/src/subagents/index.d.ts +29 -0
  348. package/dist/src/subagents/index.js +15 -0
  349. package/dist/src/subagents/index.js.map +1 -0
  350. package/dist/src/subagents/subagent-events.d.ts +93 -0
  351. package/dist/src/subagents/subagent-events.js +31 -0
  352. package/dist/src/subagents/subagent-events.js.map +1 -0
  353. package/dist/src/subagents/subagent-hooks.d.ts +29 -0
  354. package/dist/src/subagents/subagent-hooks.js +7 -0
  355. package/dist/src/subagents/subagent-hooks.js.map +1 -0
  356. package/dist/src/subagents/subagent-manager.d.ts +170 -0
  357. package/dist/src/subagents/subagent-manager.js +593 -0
  358. package/dist/src/subagents/subagent-manager.js.map +1 -0
  359. package/dist/src/subagents/subagent-manager.test.d.ts +6 -0
  360. package/dist/src/subagents/subagent-manager.test.js +822 -0
  361. package/dist/src/subagents/subagent-manager.test.js.map +1 -0
  362. package/dist/src/subagents/subagent-statistics.d.ts +46 -0
  363. package/dist/src/subagents/subagent-statistics.js +193 -0
  364. package/dist/src/subagents/subagent-statistics.js.map +1 -0
  365. package/dist/src/subagents/subagent-statistics.test.d.ts +6 -0
  366. package/dist/src/subagents/subagent-statistics.test.js +231 -0
  367. package/dist/src/subagents/subagent-statistics.test.js.map +1 -0
  368. package/dist/src/subagents/subagent.d.ts +161 -0
  369. package/dist/src/subagents/subagent.js +667 -0
  370. package/dist/src/subagents/subagent.js.map +1 -0
  371. package/dist/src/subagents/subagent.test.d.ts +6 -0
  372. package/dist/src/subagents/subagent.test.js +487 -0
  373. package/dist/src/subagents/subagent.test.js.map +1 -0
  374. package/dist/src/subagents/types.d.ts +218 -0
  375. package/dist/src/subagents/types.js +58 -0
  376. package/dist/src/subagents/types.js.map +1 -0
  377. package/dist/src/subagents/types.test.d.ts +6 -0
  378. package/dist/src/subagents/types.test.js +31 -0
  379. package/dist/src/subagents/types.test.js.map +1 -0
  380. package/dist/src/subagents/validation.d.ts +63 -0
  381. package/dist/src/subagents/validation.js +293 -0
  382. package/dist/src/subagents/validation.js.map +1 -0
  383. package/dist/src/subagents/validation.test.d.ts +6 -0
  384. package/dist/src/subagents/validation.test.js +330 -0
  385. package/dist/src/subagents/validation.test.js.map +1 -0
  386. package/dist/src/telemetry/blackbox-logger/blackbox-logger.d.ts +80 -0
  387. package/dist/src/telemetry/blackbox-logger/blackbox-logger.js +583 -0
  388. package/dist/src/telemetry/blackbox-logger/blackbox-logger.js.map +1 -0
  389. package/dist/src/telemetry/blackbox-logger/blackbox-logger.test.d.ts +6 -0
  390. package/dist/src/telemetry/blackbox-logger/blackbox-logger.test.js +301 -0
  391. package/dist/src/telemetry/blackbox-logger/blackbox-logger.test.js.map +1 -0
  392. package/dist/src/telemetry/blackbox-logger/event-types.d.ts +73 -0
  393. package/dist/src/telemetry/blackbox-logger/event-types.js +2 -0
  394. package/dist/src/telemetry/blackbox-logger/event-types.js.map +1 -0
  395. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +121 -0
  396. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +773 -0
  397. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -0
  398. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.d.ts +17 -0
  399. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +420 -0
  400. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -0
  401. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +90 -0
  402. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +229 -0
  403. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -0
  404. package/dist/src/telemetry/constants.d.ts +34 -0
  405. package/dist/src/telemetry/constants.js +35 -0
  406. package/dist/src/telemetry/constants.js.map +1 -0
  407. package/dist/src/telemetry/file-exporters.d.ts +29 -0
  408. package/dist/src/telemetry/file-exporters.js +62 -0
  409. package/dist/src/telemetry/file-exporters.js.map +1 -0
  410. package/dist/src/telemetry/index.d.ts +20 -0
  411. package/dist/src/telemetry/index.js +21 -0
  412. package/dist/src/telemetry/index.js.map +1 -0
  413. package/dist/src/telemetry/integration.test.circular.d.ts +6 -0
  414. package/dist/src/telemetry/integration.test.circular.js +95 -0
  415. package/dist/src/telemetry/integration.test.circular.js.map +1 -0
  416. package/dist/src/telemetry/loggers.d.ts +26 -0
  417. package/dist/src/telemetry/loggers.js +406 -0
  418. package/dist/src/telemetry/loggers.js.map +1 -0
  419. package/dist/src/telemetry/loggers.test.circular.d.ts +6 -0
  420. package/dist/src/telemetry/loggers.test.circular.js +107 -0
  421. package/dist/src/telemetry/loggers.test.circular.js.map +1 -0
  422. package/dist/src/telemetry/loggers.test.d.ts +6 -0
  423. package/dist/src/telemetry/loggers.test.js +638 -0
  424. package/dist/src/telemetry/loggers.test.js.map +1 -0
  425. package/dist/src/telemetry/metrics.d.ts +40 -0
  426. package/dist/src/telemetry/metrics.js +229 -0
  427. package/dist/src/telemetry/metrics.js.map +1 -0
  428. package/dist/src/telemetry/metrics.test.d.ts +6 -0
  429. package/dist/src/telemetry/metrics.test.js +242 -0
  430. package/dist/src/telemetry/metrics.test.js.map +1 -0
  431. package/dist/src/telemetry/sdk.d.ts +9 -0
  432. package/dist/src/telemetry/sdk.js +161 -0
  433. package/dist/src/telemetry/sdk.js.map +1 -0
  434. package/dist/src/telemetry/sdk.test.d.ts +6 -0
  435. package/dist/src/telemetry/sdk.test.js +82 -0
  436. package/dist/src/telemetry/sdk.test.js.map +1 -0
  437. package/dist/src/telemetry/telemetry-utils.d.ts +6 -0
  438. package/dist/src/telemetry/telemetry-utils.js +14 -0
  439. package/dist/src/telemetry/telemetry-utils.js.map +1 -0
  440. package/dist/src/telemetry/telemetry-utils.test.d.ts +6 -0
  441. package/dist/src/telemetry/telemetry-utils.test.js +40 -0
  442. package/dist/src/telemetry/telemetry-utils.test.js.map +1 -0
  443. package/dist/src/telemetry/telemetry.test.d.ts +6 -0
  444. package/dist/src/telemetry/telemetry.test.js +50 -0
  445. package/dist/src/telemetry/telemetry.test.js.map +1 -0
  446. package/dist/src/telemetry/tool-call-decision.d.ts +13 -0
  447. package/dist/src/telemetry/tool-call-decision.js +29 -0
  448. package/dist/src/telemetry/tool-call-decision.js.map +1 -0
  449. package/dist/src/telemetry/types.d.ts +236 -0
  450. package/dist/src/telemetry/types.js +405 -0
  451. package/dist/src/telemetry/types.js.map +1 -0
  452. package/dist/src/telemetry/uiTelemetry.d.ts +75 -0
  453. package/dist/src/telemetry/uiTelemetry.js +153 -0
  454. package/dist/src/telemetry/uiTelemetry.js.map +1 -0
  455. package/dist/src/telemetry/uiTelemetry.test.d.ts +6 -0
  456. package/dist/src/telemetry/uiTelemetry.test.js +558 -0
  457. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -0
  458. package/dist/src/test-utils/config.d.ts +17 -0
  459. package/dist/src/test-utils/config.js +32 -0
  460. package/dist/src/test-utils/config.js.map +1 -0
  461. package/dist/src/test-utils/mockWorkspaceContext.d.ts +13 -0
  462. package/dist/src/test-utils/mockWorkspaceContext.js +24 -0
  463. package/dist/src/test-utils/mockWorkspaceContext.js.map +1 -0
  464. package/dist/src/test-utils/tools.d.ts +45 -0
  465. package/dist/src/test-utils/tools.js +105 -0
  466. package/dist/src/test-utils/tools.js.map +1 -0
  467. package/dist/src/tools/diffOptions.d.ts +9 -0
  468. package/dist/src/tools/diffOptions.js +38 -0
  469. package/dist/src/tools/diffOptions.js.map +1 -0
  470. package/dist/src/tools/diffOptions.test.d.ts +6 -0
  471. package/dist/src/tools/diffOptions.test.js +119 -0
  472. package/dist/src/tools/diffOptions.test.js.map +1 -0
  473. package/dist/src/tools/edit.d.ts +56 -0
  474. package/dist/src/tools/edit.js +434 -0
  475. package/dist/src/tools/edit.js.map +1 -0
  476. package/dist/src/tools/edit.test.d.ts +6 -0
  477. package/dist/src/tools/edit.test.js +569 -0
  478. package/dist/src/tools/edit.test.js.map +1 -0
  479. package/dist/src/tools/exitPlanMode.d.ts +28 -0
  480. package/dist/src/tools/exitPlanMode.js +137 -0
  481. package/dist/src/tools/exitPlanMode.js.map +1 -0
  482. package/dist/src/tools/exitPlanMode.test.d.ts +6 -0
  483. package/dist/src/tools/exitPlanMode.test.js +219 -0
  484. package/dist/src/tools/exitPlanMode.test.js.map +1 -0
  485. package/dist/src/tools/glob.d.ts +52 -0
  486. package/dist/src/tools/glob.js +237 -0
  487. package/dist/src/tools/glob.js.map +1 -0
  488. package/dist/src/tools/glob.test.d.ts +6 -0
  489. package/dist/src/tools/glob.test.js +375 -0
  490. package/dist/src/tools/glob.test.js.map +1 -0
  491. package/dist/src/tools/grep.d.ts +51 -0
  492. package/dist/src/tools/grep.js +566 -0
  493. package/dist/src/tools/grep.js.map +1 -0
  494. package/dist/src/tools/grep.test.d.ts +6 -0
  495. package/dist/src/tools/grep.test.js +356 -0
  496. package/dist/src/tools/grep.test.js.map +1 -0
  497. package/dist/src/tools/ls.d.ts +68 -0
  498. package/dist/src/tools/ls.js +227 -0
  499. package/dist/src/tools/ls.js.map +1 -0
  500. package/dist/src/tools/ls.test.d.ts +6 -0
  501. package/dist/src/tools/ls.test.js +389 -0
  502. package/dist/src/tools/ls.test.js.map +1 -0
  503. package/dist/src/tools/mcp-client-manager.d.ts +38 -0
  504. package/dist/src/tools/mcp-client-manager.js +74 -0
  505. package/dist/src/tools/mcp-client-manager.js.map +1 -0
  506. package/dist/src/tools/mcp-client-manager.test.d.ts +6 -0
  507. package/dist/src/tools/mcp-client-manager.test.js +39 -0
  508. package/dist/src/tools/mcp-client-manager.test.js.map +1 -0
  509. package/dist/src/tools/mcp-client.d.ts +199 -0
  510. package/dist/src/tools/mcp-client.js +995 -0
  511. package/dist/src/tools/mcp-client.js.map +1 -0
  512. package/dist/src/tools/mcp-client.test.d.ts +6 -0
  513. package/dist/src/tools/mcp-client.test.js +454 -0
  514. package/dist/src/tools/mcp-client.test.js.map +1 -0
  515. package/dist/src/tools/mcp-tool.d.ts +23 -0
  516. package/dist/src/tools/mcp-tool.js +240 -0
  517. package/dist/src/tools/mcp-tool.js.map +1 -0
  518. package/dist/src/tools/mcp-tool.test.d.ts +6 -0
  519. package/dist/src/tools/mcp-tool.test.js +576 -0
  520. package/dist/src/tools/mcp-tool.test.js.map +1 -0
  521. package/dist/src/tools/memoryTool.d.ts +41 -0
  522. package/dist/src/tools/memoryTool.js +435 -0
  523. package/dist/src/tools/memoryTool.js.map +1 -0
  524. package/dist/src/tools/memoryTool.test.d.ts +6 -0
  525. package/dist/src/tools/memoryTool.test.js +420 -0
  526. package/dist/src/tools/memoryTool.test.js.map +1 -0
  527. package/dist/src/tools/modifiable-tool.d.ts +32 -0
  528. package/dist/src/tools/modifiable-tool.js +88 -0
  529. package/dist/src/tools/modifiable-tool.js.map +1 -0
  530. package/dist/src/tools/modifiable-tool.test.d.ts +6 -0
  531. package/dist/src/tools/modifiable-tool.test.js +193 -0
  532. package/dist/src/tools/modifiable-tool.test.js.map +1 -0
  533. package/dist/src/tools/read-file.d.ts +35 -0
  534. package/dist/src/tools/read-file.js +128 -0
  535. package/dist/src/tools/read-file.js.map +1 -0
  536. package/dist/src/tools/read-file.test.d.ts +6 -0
  537. package/dist/src/tools/read-file.test.js +311 -0
  538. package/dist/src/tools/read-file.test.js.map +1 -0
  539. package/dist/src/tools/read-many-files.d.ts +60 -0
  540. package/dist/src/tools/read-many-files.js +416 -0
  541. package/dist/src/tools/read-many-files.js.map +1 -0
  542. package/dist/src/tools/read-many-files.test.d.ts +6 -0
  543. package/dist/src/tools/read-many-files.test.js +565 -0
  544. package/dist/src/tools/read-many-files.test.js.map +1 -0
  545. package/dist/src/tools/ripGrep.d.ts +47 -0
  546. package/dist/src/tools/ripGrep.js +375 -0
  547. package/dist/src/tools/ripGrep.js.map +1 -0
  548. package/dist/src/tools/ripGrep.test.d.ts +6 -0
  549. package/dist/src/tools/ripGrep.test.js +874 -0
  550. package/dist/src/tools/ripGrep.test.js.map +1 -0
  551. package/dist/src/tools/shell.d.ts +23 -0
  552. package/dist/src/tools/shell.js +387 -0
  553. package/dist/src/tools/shell.js.map +1 -0
  554. package/dist/src/tools/shell.test.d.ts +6 -0
  555. package/dist/src/tools/shell.test.js +632 -0
  556. package/dist/src/tools/shell.test.js.map +1 -0
  557. package/dist/src/tools/task.d.ts +59 -0
  558. package/dist/src/tools/task.js +412 -0
  559. package/dist/src/tools/task.js.map +1 -0
  560. package/dist/src/tools/task.test.d.ts +6 -0
  561. package/dist/src/tools/task.test.js +369 -0
  562. package/dist/src/tools/task.test.js.map +1 -0
  563. package/dist/src/tools/todoWrite.d.ts +42 -0
  564. package/dist/src/tools/todoWrite.js +385 -0
  565. package/dist/src/tools/todoWrite.js.map +1 -0
  566. package/dist/src/tools/todoWrite.test.d.ts +6 -0
  567. package/dist/src/tools/todoWrite.test.js +222 -0
  568. package/dist/src/tools/todoWrite.test.js.map +1 -0
  569. package/dist/src/tools/tool-error.d.ts +43 -0
  570. package/dist/src/tools/tool-error.js +58 -0
  571. package/dist/src/tools/tool-error.js.map +1 -0
  572. package/dist/src/tools/tool-names.d.ts +23 -0
  573. package/dist/src/tools/tool-names.js +24 -0
  574. package/dist/src/tools/tool-names.js.map +1 -0
  575. package/dist/src/tools/tool-registry.d.ts +86 -0
  576. package/dist/src/tools/tool-registry.js +369 -0
  577. package/dist/src/tools/tool-registry.js.map +1 -0
  578. package/dist/src/tools/tool-registry.test.d.ts +6 -0
  579. package/dist/src/tools/tool-registry.test.js +332 -0
  580. package/dist/src/tools/tool-registry.test.js.map +1 -0
  581. package/dist/src/tools/tools.d.ts +316 -0
  582. package/dist/src/tools/tools.js +251 -0
  583. package/dist/src/tools/tools.js.map +1 -0
  584. package/dist/src/tools/tools.test.d.ts +6 -0
  585. package/dist/src/tools/tools.test.js +205 -0
  586. package/dist/src/tools/tools.test.js.map +1 -0
  587. package/dist/src/tools/web-fetch.d.ts +31 -0
  588. package/dist/src/tools/web-fetch.js +161 -0
  589. package/dist/src/tools/web-fetch.js.map +1 -0
  590. package/dist/src/tools/web-fetch.test.d.ts +6 -0
  591. package/dist/src/tools/web-fetch.test.js +133 -0
  592. package/dist/src/tools/web-fetch.test.js.map +1 -0
  593. package/dist/src/tools/web-search.d.ts +40 -0
  594. package/dist/src/tools/web-search.js +134 -0
  595. package/dist/src/tools/web-search.js.map +1 -0
  596. package/dist/src/tools/web-search.test.d.ts +6 -0
  597. package/dist/src/tools/web-search.test.js +125 -0
  598. package/dist/src/tools/web-search.test.js.map +1 -0
  599. package/dist/src/tools/write-file.d.ts +52 -0
  600. package/dist/src/tools/write-file.js +298 -0
  601. package/dist/src/tools/write-file.js.map +1 -0
  602. package/dist/src/tools/write-file.test.d.ts +6 -0
  603. package/dist/src/tools/write-file.test.js +436 -0
  604. package/dist/src/tools/write-file.test.js.map +1 -0
  605. package/dist/src/utils/LruCache.d.ts +13 -0
  606. package/dist/src/utils/LruCache.js +38 -0
  607. package/dist/src/utils/LruCache.js.map +1 -0
  608. package/dist/src/utils/bfsFileSearch.d.ts +24 -0
  609. package/dist/src/utils/bfsFileSearch.js +89 -0
  610. package/dist/src/utils/bfsFileSearch.js.map +1 -0
  611. package/dist/src/utils/bfsFileSearch.test.d.ts +6 -0
  612. package/dist/src/utils/bfsFileSearch.test.js +163 -0
  613. package/dist/src/utils/bfsFileSearch.test.js.map +1 -0
  614. package/dist/src/utils/browser.d.ts +13 -0
  615. package/dist/src/utils/browser.js +50 -0
  616. package/dist/src/utils/browser.js.map +1 -0
  617. package/dist/src/utils/editor.d.ts +28 -0
  618. package/dist/src/utils/editor.js +186 -0
  619. package/dist/src/utils/editor.js.map +1 -0
  620. package/dist/src/utils/editor.test.d.ts +6 -0
  621. package/dist/src/utils/editor.test.js +445 -0
  622. package/dist/src/utils/editor.test.js.map +1 -0
  623. package/dist/src/utils/environmentContext.d.ts +21 -0
  624. package/dist/src/utils/environmentContext.js +90 -0
  625. package/dist/src/utils/environmentContext.js.map +1 -0
  626. package/dist/src/utils/environmentContext.test.d.ts +6 -0
  627. package/dist/src/utils/environmentContext.test.js +148 -0
  628. package/dist/src/utils/environmentContext.test.js.map +1 -0
  629. package/dist/src/utils/errorParsing.d.ts +8 -0
  630. package/dist/src/utils/errorParsing.js +93 -0
  631. package/dist/src/utils/errorParsing.js.map +1 -0
  632. package/dist/src/utils/errorParsing.test.d.ts +6 -0
  633. package/dist/src/utils/errorParsing.test.js +172 -0
  634. package/dist/src/utils/errorParsing.test.js.map +1 -0
  635. package/dist/src/utils/errorReporting.d.ts +14 -0
  636. package/dist/src/utils/errorReporting.js +88 -0
  637. package/dist/src/utils/errorReporting.js.map +1 -0
  638. package/dist/src/utils/errorReporting.test.d.ts +6 -0
  639. package/dist/src/utils/errorReporting.test.js +130 -0
  640. package/dist/src/utils/errorReporting.test.js.map +1 -0
  641. package/dist/src/utils/errors.d.ts +33 -0
  642. package/dist/src/utils/errors.js +86 -0
  643. package/dist/src/utils/errors.js.map +1 -0
  644. package/dist/src/utils/fetch.d.ts +11 -0
  645. package/dist/src/utils/fetch.js +51 -0
  646. package/dist/src/utils/fetch.js.map +1 -0
  647. package/dist/src/utils/fileUtils.d.ts +53 -0
  648. package/dist/src/utils/fileUtils.js +283 -0
  649. package/dist/src/utils/fileUtils.js.map +1 -0
  650. package/dist/src/utils/fileUtils.test.d.ts +6 -0
  651. package/dist/src/utils/fileUtils.test.js +364 -0
  652. package/dist/src/utils/fileUtils.test.js.map +1 -0
  653. package/dist/src/utils/filesearch/crawlCache.d.ts +25 -0
  654. package/dist/src/utils/filesearch/crawlCache.js +57 -0
  655. package/dist/src/utils/filesearch/crawlCache.js.map +1 -0
  656. package/dist/src/utils/filesearch/crawlCache.test.d.ts +6 -0
  657. package/dist/src/utils/filesearch/crawlCache.test.js +103 -0
  658. package/dist/src/utils/filesearch/crawlCache.test.js.map +1 -0
  659. package/dist/src/utils/filesearch/crawler.d.ts +15 -0
  660. package/dist/src/utils/filesearch/crawler.js +50 -0
  661. package/dist/src/utils/filesearch/crawler.js.map +1 -0
  662. package/dist/src/utils/filesearch/crawler.test.d.ts +6 -0
  663. package/dist/src/utils/filesearch/crawler.test.js +468 -0
  664. package/dist/src/utils/filesearch/crawler.test.js.map +1 -0
  665. package/dist/src/utils/filesearch/fileSearch.d.ts +38 -0
  666. package/dist/src/utils/filesearch/fileSearch.js +191 -0
  667. package/dist/src/utils/filesearch/fileSearch.js.map +1 -0
  668. package/dist/src/utils/filesearch/fileSearch.test.d.ts +6 -0
  669. package/dist/src/utils/filesearch/fileSearch.test.js +642 -0
  670. package/dist/src/utils/filesearch/fileSearch.test.js.map +1 -0
  671. package/dist/src/utils/filesearch/ignore.d.ts +42 -0
  672. package/dist/src/utils/filesearch/ignore.js +106 -0
  673. package/dist/src/utils/filesearch/ignore.js.map +1 -0
  674. package/dist/src/utils/filesearch/ignore.test.d.ts +6 -0
  675. package/dist/src/utils/filesearch/ignore.test.js +144 -0
  676. package/dist/src/utils/filesearch/ignore.test.js.map +1 -0
  677. package/dist/src/utils/filesearch/result-cache.d.ts +33 -0
  678. package/dist/src/utils/filesearch/result-cache.js +59 -0
  679. package/dist/src/utils/filesearch/result-cache.js.map +1 -0
  680. package/dist/src/utils/filesearch/result-cache.test.d.ts +6 -0
  681. package/dist/src/utils/filesearch/result-cache.test.js +46 -0
  682. package/dist/src/utils/filesearch/result-cache.test.js.map +1 -0
  683. package/dist/src/utils/flashFallback.integration.test.d.ts +6 -0
  684. package/dist/src/utils/flashFallback.integration.test.js +119 -0
  685. package/dist/src/utils/flashFallback.integration.test.js.map +1 -0
  686. package/dist/src/utils/formatters.d.ts +6 -0
  687. package/dist/src/utils/formatters.js +16 -0
  688. package/dist/src/utils/formatters.js.map +1 -0
  689. package/dist/src/utils/generateContentResponseUtilities.d.ts +13 -0
  690. package/dist/src/utils/generateContentResponseUtilities.js +80 -0
  691. package/dist/src/utils/generateContentResponseUtilities.js.map +1 -0
  692. package/dist/src/utils/generateContentResponseUtilities.test.d.ts +6 -0
  693. package/dist/src/utils/generateContentResponseUtilities.test.js +235 -0
  694. package/dist/src/utils/generateContentResponseUtilities.test.js.map +1 -0
  695. package/dist/src/utils/getFolderStructure.d.ts +31 -0
  696. package/dist/src/utils/getFolderStructure.js +246 -0
  697. package/dist/src/utils/getFolderStructure.js.map +1 -0
  698. package/dist/src/utils/getFolderStructure.test.d.ts +6 -0
  699. package/dist/src/utils/getFolderStructure.test.js +282 -0
  700. package/dist/src/utils/getFolderStructure.test.js.map +1 -0
  701. package/dist/src/utils/getPty.d.ts +19 -0
  702. package/dist/src/utils/getPty.js +23 -0
  703. package/dist/src/utils/getPty.js.map +1 -0
  704. package/dist/src/utils/gitIgnoreParser.d.ts +20 -0
  705. package/dist/src/utils/gitIgnoreParser.js +61 -0
  706. package/dist/src/utils/gitIgnoreParser.js.map +1 -0
  707. package/dist/src/utils/gitIgnoreParser.test.d.ts +6 -0
  708. package/dist/src/utils/gitIgnoreParser.test.js +154 -0
  709. package/dist/src/utils/gitIgnoreParser.test.js.map +1 -0
  710. package/dist/src/utils/gitUtils.d.ts +17 -0
  711. package/dist/src/utils/gitUtils.js +61 -0
  712. package/dist/src/utils/gitUtils.js.map +1 -0
  713. package/dist/src/utils/ignorePatterns.d.ts +103 -0
  714. package/dist/src/utils/ignorePatterns.js +220 -0
  715. package/dist/src/utils/ignorePatterns.js.map +1 -0
  716. package/dist/src/utils/ignorePatterns.test.d.ts +6 -0
  717. package/dist/src/utils/ignorePatterns.test.js +250 -0
  718. package/dist/src/utils/ignorePatterns.test.js.map +1 -0
  719. package/dist/src/utils/installationManager.d.ts +16 -0
  720. package/dist/src/utils/installationManager.js +50 -0
  721. package/dist/src/utils/installationManager.js.map +1 -0
  722. package/dist/src/utils/installationManager.test.d.ts +6 -0
  723. package/dist/src/utils/installationManager.test.js +83 -0
  724. package/dist/src/utils/installationManager.test.js.map +1 -0
  725. package/dist/src/utils/language-detection.d.ts +6 -0
  726. package/dist/src/utils/language-detection.js +101 -0
  727. package/dist/src/utils/language-detection.js.map +1 -0
  728. package/dist/src/utils/memoryDiscovery.d.ts +15 -0
  729. package/dist/src/utils/memoryDiscovery.js +271 -0
  730. package/dist/src/utils/memoryDiscovery.js.map +1 -0
  731. package/dist/src/utils/memoryDiscovery.test.d.ts +6 -0
  732. package/dist/src/utils/memoryDiscovery.test.js +219 -0
  733. package/dist/src/utils/memoryDiscovery.test.js.map +1 -0
  734. package/dist/src/utils/memoryImportProcessor.d.ts +42 -0
  735. package/dist/src/utils/memoryImportProcessor.js +296 -0
  736. package/dist/src/utils/memoryImportProcessor.js.map +1 -0
  737. package/dist/src/utils/memoryImportProcessor.test.d.ts +6 -0
  738. package/dist/src/utils/memoryImportProcessor.test.js +573 -0
  739. package/dist/src/utils/memoryImportProcessor.test.js.map +1 -0
  740. package/dist/src/utils/messageInspectors.d.ts +8 -0
  741. package/dist/src/utils/messageInspectors.js +16 -0
  742. package/dist/src/utils/messageInspectors.js.map +1 -0
  743. package/dist/src/utils/nextSpeakerChecker.d.ts +12 -0
  744. package/dist/src/utils/nextSpeakerChecker.js +91 -0
  745. package/dist/src/utils/nextSpeakerChecker.js.map +1 -0
  746. package/dist/src/utils/nextSpeakerChecker.test.d.ts +6 -0
  747. package/dist/src/utils/nextSpeakerChecker.test.js +168 -0
  748. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -0
  749. package/dist/src/utils/openaiLogger.d.ts +42 -0
  750. package/dist/src/utils/openaiLogger.js +123 -0
  751. package/dist/src/utils/openaiLogger.js.map +1 -0
  752. package/dist/src/utils/partUtils.d.ts +35 -0
  753. package/dist/src/utils/partUtils.js +133 -0
  754. package/dist/src/utils/partUtils.js.map +1 -0
  755. package/dist/src/utils/partUtils.test.d.ts +6 -0
  756. package/dist/src/utils/partUtils.test.js +241 -0
  757. package/dist/src/utils/partUtils.test.js.map +1 -0
  758. package/dist/src/utils/pathReader.d.ts +17 -0
  759. package/dist/src/utils/pathReader.js +92 -0
  760. package/dist/src/utils/pathReader.js.map +1 -0
  761. package/dist/src/utils/pathReader.test.d.ts +6 -0
  762. package/dist/src/utils/pathReader.test.js +363 -0
  763. package/dist/src/utils/pathReader.test.js.map +1 -0
  764. package/dist/src/utils/paths.d.ts +58 -0
  765. package/dist/src/utils/paths.js +159 -0
  766. package/dist/src/utils/paths.js.map +1 -0
  767. package/dist/src/utils/paths.test.d.ts +6 -0
  768. package/dist/src/utils/paths.test.js +225 -0
  769. package/dist/src/utils/paths.test.js.map +1 -0
  770. package/dist/src/utils/projectSummary.d.ts +22 -0
  771. package/dist/src/utils/projectSummary.js +86 -0
  772. package/dist/src/utils/projectSummary.js.map +1 -0
  773. package/dist/src/utils/quotaErrorDetection.d.ts +20 -0
  774. package/dist/src/utils/quotaErrorDetection.js +114 -0
  775. package/dist/src/utils/quotaErrorDetection.js.map +1 -0
  776. package/dist/src/utils/quotaErrorDetection.test.d.ts +6 -0
  777. package/dist/src/utils/quotaErrorDetection.test.js +153 -0
  778. package/dist/src/utils/quotaErrorDetection.test.js.map +1 -0
  779. package/dist/src/utils/request-tokenizer/imageTokenizer.d.ts +112 -0
  780. package/dist/src/utils/request-tokenizer/imageTokenizer.js +401 -0
  781. package/dist/src/utils/request-tokenizer/imageTokenizer.js.map +1 -0
  782. package/dist/src/utils/request-tokenizer/imageTokenizer.test.d.ts +6 -0
  783. package/dist/src/utils/request-tokenizer/imageTokenizer.test.js +114 -0
  784. package/dist/src/utils/request-tokenizer/imageTokenizer.test.js.map +1 -0
  785. package/dist/src/utils/request-tokenizer/index.d.ts +18 -0
  786. package/dist/src/utils/request-tokenizer/index.js +30 -0
  787. package/dist/src/utils/request-tokenizer/index.js.map +1 -0
  788. package/dist/src/utils/request-tokenizer/requestTokenizer.d.ts +56 -0
  789. package/dist/src/utils/request-tokenizer/requestTokenizer.js +263 -0
  790. package/dist/src/utils/request-tokenizer/requestTokenizer.js.map +1 -0
  791. package/dist/src/utils/request-tokenizer/requestTokenizer.test.d.ts +6 -0
  792. package/dist/src/utils/request-tokenizer/requestTokenizer.test.js +245 -0
  793. package/dist/src/utils/request-tokenizer/requestTokenizer.test.js.map +1 -0
  794. package/dist/src/utils/request-tokenizer/supportedImageFormats.d.ts +30 -0
  795. package/dist/src/utils/request-tokenizer/supportedImageFormats.js +41 -0
  796. package/dist/src/utils/request-tokenizer/supportedImageFormats.js.map +1 -0
  797. package/dist/src/utils/request-tokenizer/textTokenizer.d.ts +29 -0
  798. package/dist/src/utils/request-tokenizer/textTokenizer.js +88 -0
  799. package/dist/src/utils/request-tokenizer/textTokenizer.js.map +1 -0
  800. package/dist/src/utils/request-tokenizer/textTokenizer.test.d.ts +6 -0
  801. package/dist/src/utils/request-tokenizer/textTokenizer.test.js +253 -0
  802. package/dist/src/utils/request-tokenizer/textTokenizer.test.js.map +1 -0
  803. package/dist/src/utils/request-tokenizer/types.d.ts +55 -0
  804. package/dist/src/utils/request-tokenizer/types.js +7 -0
  805. package/dist/src/utils/request-tokenizer/types.js.map +1 -0
  806. package/dist/src/utils/retry.d.ts +30 -0
  807. package/dist/src/utils/retry.js +288 -0
  808. package/dist/src/utils/retry.js.map +1 -0
  809. package/dist/src/utils/retry.test.d.ts +6 -0
  810. package/dist/src/utils/retry.test.js +452 -0
  811. package/dist/src/utils/retry.test.js.map +1 -0
  812. package/dist/src/utils/safeJsonParse.d.ts +15 -0
  813. package/dist/src/utils/safeJsonParse.js +41 -0
  814. package/dist/src/utils/safeJsonParse.js.map +1 -0
  815. package/dist/src/utils/safeJsonParse.test.d.ts +6 -0
  816. package/dist/src/utils/safeJsonParse.test.js +112 -0
  817. package/dist/src/utils/safeJsonParse.test.js.map +1 -0
  818. package/dist/src/utils/safeJsonStringify.d.ts +13 -0
  819. package/dist/src/utils/safeJsonStringify.js +25 -0
  820. package/dist/src/utils/safeJsonStringify.js.map +1 -0
  821. package/dist/src/utils/safeJsonStringify.test.d.ts +6 -0
  822. package/dist/src/utils/safeJsonStringify.test.js +61 -0
  823. package/dist/src/utils/safeJsonStringify.test.js.map +1 -0
  824. package/dist/src/utils/schemaValidator.d.ts +15 -0
  825. package/dist/src/utils/schemaValidator.js +57 -0
  826. package/dist/src/utils/schemaValidator.js.map +1 -0
  827. package/dist/src/utils/secure-browser-launcher.d.ts +23 -0
  828. package/dist/src/utils/secure-browser-launcher.js +165 -0
  829. package/dist/src/utils/secure-browser-launcher.js.map +1 -0
  830. package/dist/src/utils/secure-browser-launcher.test.d.ts +6 -0
  831. package/dist/src/utils/secure-browser-launcher.test.js +149 -0
  832. package/dist/src/utils/secure-browser-launcher.test.js.map +1 -0
  833. package/dist/src/utils/session.d.ts +6 -0
  834. package/dist/src/utils/session.js +8 -0
  835. package/dist/src/utils/session.js.map +1 -0
  836. package/dist/src/utils/shell-utils.d.ts +121 -0
  837. package/dist/src/utils/shell-utils.js +381 -0
  838. package/dist/src/utils/shell-utils.js.map +1 -0
  839. package/dist/src/utils/shell-utils.test.d.ts +6 -0
  840. package/dist/src/utils/shell-utils.test.js +346 -0
  841. package/dist/src/utils/shell-utils.test.js.map +1 -0
  842. package/dist/src/utils/shellReadOnlyChecker.d.ts +6 -0
  843. package/dist/src/utils/shellReadOnlyChecker.js +247 -0
  844. package/dist/src/utils/shellReadOnlyChecker.js.map +1 -0
  845. package/dist/src/utils/shellReadOnlyChecker.test.d.ts +6 -0
  846. package/dist/src/utils/shellReadOnlyChecker.test.js +47 -0
  847. package/dist/src/utils/shellReadOnlyChecker.test.js.map +1 -0
  848. package/dist/src/utils/subagentGenerator.d.ts +20 -0
  849. package/dist/src/utils/subagentGenerator.js +116 -0
  850. package/dist/src/utils/subagentGenerator.js.map +1 -0
  851. package/dist/src/utils/subagentGenerator.test.d.ts +6 -0
  852. package/dist/src/utils/subagentGenerator.test.js +158 -0
  853. package/dist/src/utils/subagentGenerator.test.js.map +1 -0
  854. package/dist/src/utils/summarizer.d.ts +25 -0
  855. package/dist/src/utils/summarizer.js +51 -0
  856. package/dist/src/utils/summarizer.js.map +1 -0
  857. package/dist/src/utils/summarizer.test.d.ts +6 -0
  858. package/dist/src/utils/summarizer.test.js +131 -0
  859. package/dist/src/utils/summarizer.test.js.map +1 -0
  860. package/dist/src/utils/systemEncoding.d.ts +40 -0
  861. package/dist/src/utils/systemEncoding.js +149 -0
  862. package/dist/src/utils/systemEncoding.js.map +1 -0
  863. package/dist/src/utils/systemEncoding.test.d.ts +6 -0
  864. package/dist/src/utils/systemEncoding.test.js +368 -0
  865. package/dist/src/utils/systemEncoding.test.js.map +1 -0
  866. package/dist/src/utils/testUtils.d.ts +29 -0
  867. package/dist/src/utils/testUtils.js +70 -0
  868. package/dist/src/utils/testUtils.js.map +1 -0
  869. package/dist/src/utils/textUtils.d.ts +13 -0
  870. package/dist/src/utils/textUtils.js +28 -0
  871. package/dist/src/utils/textUtils.js.map +1 -0
  872. package/dist/src/utils/tool-utils.d.ts +19 -0
  873. package/dist/src/utils/tool-utils.js +58 -0
  874. package/dist/src/utils/tool-utils.js.map +1 -0
  875. package/dist/src/utils/tool-utils.test.d.ts +6 -0
  876. package/dist/src/utils/tool-utils.test.js +61 -0
  877. package/dist/src/utils/tool-utils.test.js.map +1 -0
  878. package/dist/src/utils/userAccountManager.d.ts +20 -0
  879. package/dist/src/utils/userAccountManager.js +114 -0
  880. package/dist/src/utils/userAccountManager.js.map +1 -0
  881. package/dist/src/utils/userAccountManager.test.d.ts +6 -0
  882. package/dist/src/utils/userAccountManager.test.js +223 -0
  883. package/dist/src/utils/userAccountManager.test.js.map +1 -0
  884. package/dist/src/utils/workspaceContext.d.ts +66 -0
  885. package/dist/src/utils/workspaceContext.js +171 -0
  886. package/dist/src/utils/workspaceContext.js.map +1 -0
  887. package/dist/src/utils/workspaceContext.test.d.ts +6 -0
  888. package/dist/src/utils/workspaceContext.test.js +318 -0
  889. package/dist/src/utils/workspaceContext.test.js.map +1 -0
  890. package/dist/src/utils/yaml-parser.d.ts +29 -0
  891. package/dist/src/utils/yaml-parser.js +172 -0
  892. package/dist/src/utils/yaml-parser.js.map +1 -0
  893. package/dist/src/utils/yaml-parser.test.d.ts +6 -0
  894. package/dist/src/utils/yaml-parser.test.js +170 -0
  895. package/dist/src/utils/yaml-parser.test.js.map +1 -0
  896. package/dist/tsconfig.tsbuildinfo +1 -0
  897. package/package.json +89 -0
@@ -0,0 +1,2059 @@
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 { GoogleGenAI } from '@google/genai';
8
+ import { findIndexAfterFraction, GeminiClient } from './client.js';
9
+ import { getPlanModeSystemReminder } from './prompts.js';
10
+ import { AuthType, } from './contentGenerator.js';
11
+ import {} from './geminiChat.js';
12
+ import { Config } from '../config/config.js';
13
+ import { CompressionStatus, GeminiEventType, Turn, } from './turn.js';
14
+ import { getCoreSystemPrompt } from './prompts.js';
15
+ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
16
+ import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
17
+ import { setSimulate429 } from '../utils/testUtils.js';
18
+ import { tokenLimit } from './tokenLimits.js';
19
+ import { ideContext } from '../ide/ideContext.js';
20
+ import { BlackboxLogger } from '../telemetry/blackbox-logger/blackbox-logger.js';
21
+ // --- Mocks ---
22
+ const mockChatCreateFn = vi.fn();
23
+ const mockGenerateContentFn = vi.fn();
24
+ const mockEmbedContentFn = vi.fn();
25
+ const mockTurnRunFn = vi.fn();
26
+ let ApprovalModeEnum;
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ let mockConfigObject;
29
+ vi.mock('@google/genai');
30
+ vi.mock('./turn', async (importOriginal) => {
31
+ const actual = await importOriginal();
32
+ // Define a mock class that has the same shape as the real Turn
33
+ class MockTurn {
34
+ pendingToolCalls = [];
35
+ // The run method is a property that holds our mock function
36
+ run = mockTurnRunFn;
37
+ constructor() {
38
+ // The constructor can be empty or do some mock setup
39
+ }
40
+ }
41
+ // Export the mock class as 'Turn'
42
+ return {
43
+ ...actual,
44
+ Turn: MockTurn,
45
+ };
46
+ });
47
+ vi.mock('../config/config.js');
48
+ vi.mock('./prompts');
49
+ vi.mock('../utils/getFolderStructure', () => ({
50
+ getFolderStructure: vi.fn().mockResolvedValue('Mock Folder Structure'),
51
+ }));
52
+ vi.mock('../utils/errorReporting', () => ({ reportError: vi.fn() }));
53
+ vi.mock('../utils/nextSpeakerChecker', () => ({
54
+ checkNextSpeaker: vi.fn().mockResolvedValue(null),
55
+ }));
56
+ vi.mock('../utils/generateContentResponseUtilities', () => ({
57
+ getResponseText: (result) => result.candidates?.[0]?.content?.parts?.map((part) => part.text).join('') ||
58
+ undefined,
59
+ getFunctionCalls: (result) => {
60
+ // Extract function calls from the response
61
+ const parts = result.candidates?.[0]?.content?.parts;
62
+ if (!parts) {
63
+ return undefined;
64
+ }
65
+ const functionCallParts = parts
66
+ .filter((part) => !!part.functionCall)
67
+ .map((part) => part.functionCall);
68
+ return functionCallParts.length > 0 ? functionCallParts : undefined;
69
+ },
70
+ }));
71
+ vi.mock('../telemetry/index.js', () => ({
72
+ logApiRequest: vi.fn(),
73
+ logApiResponse: vi.fn(),
74
+ logApiError: vi.fn(),
75
+ }));
76
+ vi.mock('../ide/ideContext.js');
77
+ /**
78
+ * Array.fromAsync ponyfill, which will be available in es 2024.
79
+ *
80
+ * Buffers an async generator into an array and returns the result.
81
+ */
82
+ async function fromAsync(promise) {
83
+ const results = [];
84
+ for await (const result of promise) {
85
+ results.push(result);
86
+ }
87
+ return results;
88
+ }
89
+ describe('findIndexAfterFraction', () => {
90
+ const history = [
91
+ { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66
92
+ { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68
93
+ { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66
94
+ { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68
95
+ { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65
96
+ ];
97
+ // Total length: 333
98
+ it('should throw an error for non-positive numbers', () => {
99
+ expect(() => findIndexAfterFraction(history, 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(() => findIndexAfterFraction(history, 1)).toThrow('Fraction must be between 0 and 1');
103
+ });
104
+ it('should handle a fraction in the middle', () => {
105
+ // 333 * 0.5 = 166.5
106
+ // 0: 66
107
+ // 1: 66 + 68 = 134
108
+ // 2: 134 + 66 = 200
109
+ // 200 >= 166.5, so index is 2
110
+ expect(findIndexAfterFraction(history, 0.5)).toBe(2);
111
+ });
112
+ it('should handle a fraction that results in the last index', () => {
113
+ // 333 * 0.9 = 299.7
114
+ // ...
115
+ // 3: 200 + 68 = 268
116
+ // 4: 268 + 65 = 333
117
+ // 333 >= 299.7, so index is 4
118
+ expect(findIndexAfterFraction(history, 0.9)).toBe(4);
119
+ });
120
+ it('should handle an empty history', () => {
121
+ expect(findIndexAfterFraction([], 0.5)).toBe(0);
122
+ });
123
+ it('should handle a history with only one item', () => {
124
+ expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(0);
125
+ });
126
+ it('should handle history with weird parts', () => {
127
+ const historyWithEmptyParts = [
128
+ { role: 'user', parts: [{ text: 'Message 1' }] },
129
+ { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
130
+ { role: 'user', parts: [{ text: 'Message 2' }] },
131
+ ];
132
+ expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(1);
133
+ });
134
+ });
135
+ describe('Gemini Client (client.ts)', () => {
136
+ let client;
137
+ beforeEach(async () => {
138
+ vi.resetAllMocks();
139
+ ApprovalModeEnum = (await vi.importActual('../config/config.js')).ApprovalMode;
140
+ // Disable 429 simulation for tests
141
+ setSimulate429(false);
142
+ // Set up the mock for GoogleGenAI constructor and its methods
143
+ const MockedGoogleGenAI = vi.mocked(GoogleGenAI);
144
+ MockedGoogleGenAI.mockImplementation(() => {
145
+ const mock = {
146
+ chats: { create: mockChatCreateFn },
147
+ models: {
148
+ generateContent: mockGenerateContentFn,
149
+ embedContent: mockEmbedContentFn,
150
+ },
151
+ };
152
+ return mock;
153
+ });
154
+ mockChatCreateFn.mockResolvedValue({});
155
+ mockGenerateContentFn.mockResolvedValue({
156
+ candidates: [
157
+ {
158
+ content: {
159
+ parts: [
160
+ {
161
+ functionCall: {
162
+ name: 'respond_in_schema',
163
+ args: { key: 'value' },
164
+ },
165
+ },
166
+ ],
167
+ },
168
+ },
169
+ ],
170
+ });
171
+ // Because the GeminiClient constructor kicks off an async process (startChat)
172
+ // that depends on a fully-formed Config object, we need to mock the
173
+ // entire implementation of Config for these tests.
174
+ const mockToolRegistry = {
175
+ getFunctionDeclarations: vi.fn().mockReturnValue([]),
176
+ getTool: vi.fn().mockReturnValue(null),
177
+ };
178
+ const fileService = new FileDiscoveryService('/test/dir');
179
+ const contentGeneratorConfig = {
180
+ model: 'test-model',
181
+ apiKey: 'test-key',
182
+ vertexai: false,
183
+ authType: AuthType.USE_GEMINI,
184
+ };
185
+ const mockSubagentManager = {
186
+ listSubagents: vi.fn().mockResolvedValue([]),
187
+ addChangeListener: vi.fn().mockReturnValue(() => { }),
188
+ };
189
+ mockConfigObject = {
190
+ getContentGeneratorConfig: vi
191
+ .fn()
192
+ .mockReturnValue(contentGeneratorConfig),
193
+ getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
194
+ getModel: vi.fn().mockReturnValue('test-model'),
195
+ getEmbeddingModel: vi.fn().mockReturnValue('test-embedding-model'),
196
+ getApiKey: vi.fn().mockReturnValue('test-key'),
197
+ getVertexAI: vi.fn().mockReturnValue(false),
198
+ getUserAgent: vi.fn().mockReturnValue('test-agent'),
199
+ getUserMemory: vi.fn().mockReturnValue(''),
200
+ getFullContext: vi.fn().mockReturnValue(false),
201
+ getSessionId: vi.fn().mockReturnValue('test-session-id'),
202
+ getProxy: vi.fn().mockReturnValue(undefined),
203
+ getWorkingDir: vi.fn().mockReturnValue('/test/dir'),
204
+ getFileService: vi.fn().mockReturnValue(fileService),
205
+ getMaxSessionTurns: vi.fn().mockReturnValue(0),
206
+ getSessionTokenLimit: vi.fn().mockReturnValue(32000),
207
+ getQuotaErrorOccurred: vi.fn().mockReturnValue(false),
208
+ setQuotaErrorOccurred: vi.fn(),
209
+ getNoBrowser: vi.fn().mockReturnValue(false),
210
+ getSystemPromptMappings: vi.fn().mockReturnValue(undefined),
211
+ getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
212
+ getApprovalMode: vi.fn().mockReturnValue(ApprovalModeEnum.DEFAULT),
213
+ getIdeModeFeature: vi.fn().mockReturnValue(false),
214
+ getIdeMode: vi.fn().mockReturnValue(true),
215
+ getDebugMode: vi.fn().mockReturnValue(false),
216
+ getWorkspaceContext: vi.fn().mockReturnValue({
217
+ getDirectories: vi.fn().mockReturnValue(['/test/dir']),
218
+ }),
219
+ getGeminiClient: vi.fn(),
220
+ setFallbackMode: vi.fn(),
221
+ getCliVersion: vi.fn().mockReturnValue('1.0.0'),
222
+ getChatCompression: vi.fn().mockReturnValue(undefined),
223
+ getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
224
+ getSubagentManager: vi.fn().mockReturnValue(mockSubagentManager),
225
+ getSkipLoopDetection: vi.fn().mockReturnValue(false),
226
+ };
227
+ const MockedConfig = vi.mocked(Config, true);
228
+ MockedConfig.mockImplementation(() => mockConfigObject);
229
+ // We can instantiate the client here since Config is mocked
230
+ // and the constructor will use the mocked GoogleGenAI
231
+ client = new GeminiClient(new Config({ sessionId: 'test-session-id' }));
232
+ mockConfigObject.getGeminiClient.mockReturnValue(client);
233
+ await client.initialize(contentGeneratorConfig);
234
+ });
235
+ afterEach(() => {
236
+ vi.restoreAllMocks();
237
+ });
238
+ // NOTE: The following tests for startChat were removed due to persistent issues with
239
+ // the @google/genai mock. Specifically, the mockChatCreateFn (representing instance.chats.create)
240
+ // was not being detected as called by the GeminiClient instance.
241
+ // This likely points to a subtle issue in how the GoogleGenerativeAI class constructor
242
+ // and its instance methods are mocked and then used by the class under test.
243
+ // For future debugging, ensure that the `this.client` in `GeminiClient` (which is an
244
+ // instance of the mocked GoogleGenerativeAI) correctly has its `chats.create` method
245
+ // pointing to `mockChatCreateFn`.
246
+ // it('startChat should call getCoreSystemPrompt with userMemory and pass to chats.create', async () => { ... });
247
+ // it('startChat should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
248
+ // NOTE: The following tests for generateJson were removed due to persistent issues with
249
+ // the @google/genai mock, similar to the startChat tests. The mockGenerateContentFn
250
+ // (representing instance.models.generateContent) was not being detected as called, or the mock
251
+ // was not preventing an actual API call (leading to API key errors).
252
+ // For future debugging, ensure `this.client.models.generateContent` in `GeminiClient` correctly
253
+ // uses the `mockGenerateContentFn`.
254
+ // it('generateJson should call getCoreSystemPrompt with userMemory and pass to generateContent', async () => { ... });
255
+ // it('generateJson should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
256
+ describe('generateEmbedding', () => {
257
+ const texts = ['hello world', 'goodbye world'];
258
+ const testEmbeddingModel = 'test-embedding-model';
259
+ it('should call embedContent with correct parameters and return embeddings', async () => {
260
+ const mockEmbeddings = [
261
+ [0.1, 0.2, 0.3],
262
+ [0.4, 0.5, 0.6],
263
+ ];
264
+ const mockResponse = {
265
+ embeddings: [
266
+ { values: mockEmbeddings[0] },
267
+ { values: mockEmbeddings[1] },
268
+ ],
269
+ };
270
+ mockEmbedContentFn.mockResolvedValue(mockResponse);
271
+ const result = await client.generateEmbedding(texts);
272
+ expect(mockEmbedContentFn).toHaveBeenCalledTimes(1);
273
+ expect(mockEmbedContentFn).toHaveBeenCalledWith({
274
+ model: testEmbeddingModel,
275
+ contents: texts,
276
+ });
277
+ expect(result).toEqual(mockEmbeddings);
278
+ });
279
+ it('should return an empty array if an empty array is passed', async () => {
280
+ const result = await client.generateEmbedding([]);
281
+ expect(result).toEqual([]);
282
+ expect(mockEmbedContentFn).not.toHaveBeenCalled();
283
+ });
284
+ it('should throw an error if API response has no embeddings array', async () => {
285
+ mockEmbedContentFn.mockResolvedValue({}); // No `embeddings` key
286
+ await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
287
+ });
288
+ it('should throw an error if API response has an empty embeddings array', async () => {
289
+ const mockResponse = {
290
+ embeddings: [],
291
+ };
292
+ mockEmbedContentFn.mockResolvedValue(mockResponse);
293
+ await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
294
+ });
295
+ it('should throw an error if API returns a mismatched number of embeddings', async () => {
296
+ const mockResponse = {
297
+ embeddings: [{ values: [1, 2, 3] }], // Only one for two texts
298
+ };
299
+ mockEmbedContentFn.mockResolvedValue(mockResponse);
300
+ await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned a mismatched number of embeddings. Expected 2, got 1.');
301
+ });
302
+ it('should throw an error if any embedding has nullish values', async () => {
303
+ const mockResponse = {
304
+ embeddings: [{ values: [1, 2, 3] }, { values: undefined }], // Second one is bad
305
+ };
306
+ mockEmbedContentFn.mockResolvedValue(mockResponse);
307
+ await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 1: "goodbye world"');
308
+ });
309
+ it('should throw an error if any embedding has an empty values array', async () => {
310
+ const mockResponse = {
311
+ embeddings: [{ values: [] }, { values: [1, 2, 3] }], // First one is bad
312
+ };
313
+ mockEmbedContentFn.mockResolvedValue(mockResponse);
314
+ await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 0: "hello world"');
315
+ });
316
+ it('should propagate errors from the API call', async () => {
317
+ const apiError = new Error('API Failure');
318
+ mockEmbedContentFn.mockRejectedValue(apiError);
319
+ await expect(client.generateEmbedding(texts)).rejects.toThrow('API Failure');
320
+ });
321
+ });
322
+ describe('generateJson', () => {
323
+ it('should call generateContent with the correct parameters', async () => {
324
+ const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
325
+ const schema = { type: 'string' };
326
+ const abortSignal = new AbortController().signal;
327
+ // Mock countTokens
328
+ const mockGenerator = {
329
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
330
+ generateContent: mockGenerateContentFn,
331
+ };
332
+ client['contentGenerator'] = mockGenerator;
333
+ const result = await client.generateJson(contents, schema, abortSignal);
334
+ expect(result).toEqual({ key: 'value' });
335
+ expect(mockGenerateContentFn).toHaveBeenCalledWith({
336
+ model: 'test-model', // Should use current model from config
337
+ config: {
338
+ abortSignal,
339
+ systemInstruction: getCoreSystemPrompt(''),
340
+ temperature: 0,
341
+ topP: 1,
342
+ tools: [
343
+ {
344
+ functionDeclarations: [
345
+ {
346
+ name: 'respond_in_schema',
347
+ description: 'Provide the response in provided schema',
348
+ parameters: schema,
349
+ },
350
+ ],
351
+ },
352
+ ],
353
+ },
354
+ contents,
355
+ }, 'test-session-id');
356
+ });
357
+ /* We now use model in contentGeneratorConfig in most cases. */
358
+ it.skip('should allow overriding model and config', async () => {
359
+ const contents = [
360
+ { role: 'user', parts: [{ text: 'hello' }] },
361
+ ];
362
+ const schema = { type: 'string' };
363
+ const abortSignal = new AbortController().signal;
364
+ const customModel = 'custom-json-model';
365
+ const customConfig = { temperature: 0.9, topK: 20 };
366
+ const mockGenerator = {
367
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
368
+ generateContent: mockGenerateContentFn,
369
+ };
370
+ client['contentGenerator'] = mockGenerator;
371
+ const result = await client.generateJson(contents, schema, abortSignal, customModel, customConfig);
372
+ expect(result).toEqual({ key: 'value' });
373
+ expect(mockGenerateContentFn).toHaveBeenCalledWith({
374
+ model: customModel,
375
+ config: {
376
+ abortSignal,
377
+ systemInstruction: getCoreSystemPrompt(''),
378
+ temperature: 0.9,
379
+ topP: 1, // from default
380
+ topK: 20,
381
+ tools: [
382
+ {
383
+ functionDeclarations: [
384
+ {
385
+ name: 'respond_in_schema',
386
+ description: 'Provide the response in provided schema',
387
+ parameters: schema,
388
+ },
389
+ ],
390
+ },
391
+ ],
392
+ },
393
+ contents,
394
+ }, 'test-session-id');
395
+ });
396
+ });
397
+ describe('addHistory', () => {
398
+ it('should call chat.addHistory with the provided content', async () => {
399
+ const mockChat = {
400
+ addHistory: vi.fn(),
401
+ };
402
+ client['chat'] = mockChat;
403
+ const newContent = {
404
+ role: 'user',
405
+ parts: [{ text: 'New history item' }],
406
+ };
407
+ await client.addHistory(newContent);
408
+ expect(mockChat.addHistory).toHaveBeenCalledWith(newContent);
409
+ });
410
+ });
411
+ describe('resetChat', () => {
412
+ it('should create a new chat session, clearing the old history', async () => {
413
+ // 1. Get the initial chat instance and add some history.
414
+ const initialChat = client.getChat();
415
+ const initialHistory = await client.getHistory();
416
+ await client.addHistory({
417
+ role: 'user',
418
+ parts: [{ text: 'some old message' }],
419
+ });
420
+ const historyWithOldMessage = await client.getHistory();
421
+ expect(historyWithOldMessage.length).toBeGreaterThan(initialHistory.length);
422
+ // 2. Call resetChat.
423
+ await client.resetChat();
424
+ // 3. Get the new chat instance and its history.
425
+ const newChat = client.getChat();
426
+ const newHistory = await client.getHistory();
427
+ // 4. Assert that the chat instance is new and the history is reset.
428
+ expect(newChat).not.toBe(initialChat);
429
+ expect(newHistory.length).toBe(initialHistory.length);
430
+ expect(JSON.stringify(newHistory)).not.toContain('some old message');
431
+ });
432
+ });
433
+ describe('tryCompressChat', () => {
434
+ const mockCountTokens = vi.fn();
435
+ const mockSendMessage = vi.fn();
436
+ const mockGetHistory = vi.fn();
437
+ beforeEach(() => {
438
+ vi.mock('./tokenLimits', () => ({
439
+ tokenLimit: vi.fn(),
440
+ }));
441
+ client['contentGenerator'] = {
442
+ countTokens: mockCountTokens,
443
+ };
444
+ client['chat'] = {
445
+ getHistory: mockGetHistory,
446
+ addHistory: vi.fn(),
447
+ setHistory: vi.fn(),
448
+ sendMessage: mockSendMessage,
449
+ };
450
+ });
451
+ function setup({ chatHistory = [
452
+ { role: 'user', parts: [{ text: 'Long conversation' }] },
453
+ { role: 'model', parts: [{ text: 'Long response' }] },
454
+ ], } = {}) {
455
+ const mockChat = {
456
+ getHistory: vi.fn().mockReturnValue(chatHistory),
457
+ setHistory: vi.fn(),
458
+ sendMessage: vi.fn().mockResolvedValue({ text: 'Summary' }),
459
+ };
460
+ const mockCountTokens = vi
461
+ .fn()
462
+ .mockResolvedValueOnce({ totalTokens: 1000 })
463
+ .mockResolvedValueOnce({ totalTokens: 5000 });
464
+ const mockGenerator = {
465
+ countTokens: mockCountTokens,
466
+ };
467
+ client['chat'] = mockChat;
468
+ client['contentGenerator'] = mockGenerator;
469
+ client['startChat'] = vi.fn().mockResolvedValue({ ...mockChat });
470
+ return { client, mockChat, mockGenerator };
471
+ }
472
+ describe('when compression inflates the token count', () => {
473
+ it('uses the truncated history for compression');
474
+ it('allows compression to be forced/manual after a failure', async () => {
475
+ const { client, mockGenerator } = setup();
476
+ mockGenerator.countTokens?.mockResolvedValue({
477
+ totalTokens: 1000,
478
+ });
479
+ await client.tryCompressChat('prompt-id-4'); // Fails
480
+ const result = await client.tryCompressChat('prompt-id-4', true);
481
+ expect(result).toEqual({
482
+ compressionStatus: CompressionStatus.COMPRESSED,
483
+ newTokenCount: 1000,
484
+ originalTokenCount: 1000,
485
+ });
486
+ });
487
+ it('yields the result even if the compression inflated the tokens', async () => {
488
+ const { client } = setup();
489
+ const result = await client.tryCompressChat('prompt-id-4', true);
490
+ expect(result).toEqual({
491
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
492
+ newTokenCount: 5000,
493
+ originalTokenCount: 1000,
494
+ });
495
+ });
496
+ it('does not manipulate the source chat', async () => {
497
+ const { client, mockChat } = setup();
498
+ await client.tryCompressChat('prompt-id-4', true);
499
+ expect(client['chat']).toBe(mockChat); // a new chat session was not created
500
+ });
501
+ it('restores the history back to the original', async () => {
502
+ vi.mocked(tokenLimit).mockReturnValue(1000);
503
+ mockCountTokens.mockResolvedValue({
504
+ totalTokens: 999,
505
+ });
506
+ const originalHistory = [
507
+ { role: 'user', parts: [{ text: 'what is your wisdom?' }] },
508
+ { role: 'model', parts: [{ text: 'some wisdom' }] },
509
+ { role: 'user', parts: [{ text: 'ahh that is a good a wisdom' }] },
510
+ ];
511
+ const { client } = setup({
512
+ chatHistory: originalHistory,
513
+ });
514
+ const { compressionStatus } = await client.tryCompressChat('prompt-id-4');
515
+ expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
516
+ expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
517
+ });
518
+ it('will not attempt to compress context after a failure', async () => {
519
+ const { client, mockGenerator } = setup();
520
+ await client.tryCompressChat('prompt-id-4');
521
+ const result = await client.tryCompressChat('prompt-id-5');
522
+ // it counts tokens for {original, compressed} and then never again
523
+ expect(mockGenerator.countTokens).toHaveBeenCalledTimes(2);
524
+ expect(result).toEqual({
525
+ compressionStatus: CompressionStatus.NOOP,
526
+ newTokenCount: 0,
527
+ originalTokenCount: 0,
528
+ });
529
+ });
530
+ });
531
+ it('attempts to compress with a maxOutputTokens set to the original token count', async () => {
532
+ vi.mocked(tokenLimit).mockReturnValue(1000);
533
+ mockCountTokens.mockResolvedValue({
534
+ totalTokens: 999,
535
+ });
536
+ mockGetHistory.mockReturnValue([
537
+ { role: 'user', parts: [{ text: '...history...' }] },
538
+ ]);
539
+ // Mock the summary response from the chat
540
+ mockSendMessage.mockResolvedValue({
541
+ role: 'model',
542
+ parts: [{ text: 'This is a summary.' }],
543
+ });
544
+ await client.tryCompressChat('prompt-id-2', true);
545
+ expect(mockSendMessage).toHaveBeenCalledWith(expect.objectContaining({
546
+ config: expect.objectContaining({
547
+ maxOutputTokens: 999,
548
+ }),
549
+ }), 'prompt-id-2');
550
+ });
551
+ it('should not trigger summarization if token count is below threshold', async () => {
552
+ const MOCKED_TOKEN_LIMIT = 1000;
553
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
554
+ mockGetHistory.mockReturnValue([
555
+ { role: 'user', parts: [{ text: '...history...' }] },
556
+ ]);
557
+ mockCountTokens.mockResolvedValue({
558
+ totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
559
+ });
560
+ const initialChat = client.getChat();
561
+ const result = await client.tryCompressChat('prompt-id-2');
562
+ const newChat = client.getChat();
563
+ expect(tokenLimit).toHaveBeenCalled();
564
+ expect(result).toEqual({
565
+ compressionStatus: CompressionStatus.NOOP,
566
+ newTokenCount: 699,
567
+ originalTokenCount: 699,
568
+ });
569
+ expect(newChat).toBe(initialChat);
570
+ });
571
+ it('logs a telemetry event when compressing', async () => {
572
+ vi.spyOn(BlackboxLogger.prototype, 'logChatCompressionEvent');
573
+ const MOCKED_TOKEN_LIMIT = 1000;
574
+ const MOCKED_CONTEXT_PERCENTAGE_THRESHOLD = 0.5;
575
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
576
+ vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
577
+ contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
578
+ });
579
+ mockGetHistory.mockReturnValue([
580
+ { role: 'user', parts: [{ text: '...history...' }] },
581
+ ]);
582
+ const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
583
+ const newTokenCount = 100;
584
+ mockCountTokens
585
+ .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
586
+ .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
587
+ // Mock the summary response from the chat
588
+ mockSendMessage.mockResolvedValue({
589
+ role: 'model',
590
+ parts: [{ text: 'This is a summary.' }],
591
+ });
592
+ await client.tryCompressChat('prompt-id-3');
593
+ expect(BlackboxLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
594
+ tokens_before: originalTokenCount,
595
+ tokens_after: newTokenCount,
596
+ }));
597
+ });
598
+ it('should trigger summarization if token count is at threshold with contextPercentageThreshold setting', async () => {
599
+ const MOCKED_TOKEN_LIMIT = 1000;
600
+ const MOCKED_CONTEXT_PERCENTAGE_THRESHOLD = 0.5;
601
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
602
+ vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
603
+ contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
604
+ });
605
+ mockGetHistory.mockReturnValue([
606
+ { role: 'user', parts: [{ text: '...history...' }] },
607
+ ]);
608
+ const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
609
+ const newTokenCount = 100;
610
+ mockCountTokens
611
+ .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
612
+ .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
613
+ // Mock the summary response from the chat
614
+ mockSendMessage.mockResolvedValue({
615
+ role: 'model',
616
+ parts: [{ text: 'This is a summary.' }],
617
+ });
618
+ const initialChat = client.getChat();
619
+ const result = await client.tryCompressChat('prompt-id-3');
620
+ const newChat = client.getChat();
621
+ expect(tokenLimit).toHaveBeenCalled();
622
+ expect(mockSendMessage).toHaveBeenCalled();
623
+ // Assert that summarization happened and returned the correct stats
624
+ expect(result).toEqual({
625
+ compressionStatus: CompressionStatus.COMPRESSED,
626
+ originalTokenCount,
627
+ newTokenCount,
628
+ });
629
+ // Assert that the chat was reset
630
+ expect(newChat).not.toBe(initialChat);
631
+ });
632
+ it('should not compress across a function call response', async () => {
633
+ const MOCKED_TOKEN_LIMIT = 1000;
634
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
635
+ mockGetHistory.mockReturnValue([
636
+ { role: 'user', parts: [{ text: '...history 1...' }] },
637
+ { role: 'model', parts: [{ text: '...history 2...' }] },
638
+ { role: 'user', parts: [{ text: '...history 3...' }] },
639
+ { role: 'model', parts: [{ text: '...history 4...' }] },
640
+ { role: 'user', parts: [{ text: '...history 5...' }] },
641
+ { role: 'model', parts: [{ text: '...history 6...' }] },
642
+ { role: 'user', parts: [{ text: '...history 7...' }] },
643
+ { role: 'model', parts: [{ text: '...history 8...' }] },
644
+ // Normally we would break here, but we have a function response.
645
+ {
646
+ role: 'user',
647
+ parts: [{ functionResponse: { name: '...history 8...' } }],
648
+ },
649
+ { role: 'model', parts: [{ text: '...history 10...' }] },
650
+ // Instead we will break here.
651
+ { role: 'user', parts: [{ text: '...history 10...' }] },
652
+ ]);
653
+ const originalTokenCount = 1000 * 0.7;
654
+ const newTokenCount = 100;
655
+ mockCountTokens
656
+ .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
657
+ .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
658
+ // Mock the summary response from the chat
659
+ mockSendMessage.mockResolvedValue({
660
+ role: 'model',
661
+ parts: [{ text: 'This is a summary.' }],
662
+ });
663
+ const initialChat = client.getChat();
664
+ const result = await client.tryCompressChat('prompt-id-3');
665
+ const newChat = client.getChat();
666
+ expect(tokenLimit).toHaveBeenCalled();
667
+ expect(mockSendMessage).toHaveBeenCalled();
668
+ // Assert that summarization happened and returned the correct stats
669
+ expect(result).toEqual({
670
+ compressionStatus: CompressionStatus.COMPRESSED,
671
+ originalTokenCount,
672
+ newTokenCount,
673
+ });
674
+ // Assert that the chat was reset
675
+ expect(newChat).not.toBe(initialChat);
676
+ // 1. standard start context message
677
+ // 2. standard canned user start message
678
+ // 3. compressed summary message
679
+ // 4. standard canned user summary message
680
+ // 5. The last user message (not the last 3 because that would start with a function response)
681
+ expect(newChat.getHistory().length).toEqual(5);
682
+ });
683
+ it('should always trigger summarization when force is true, regardless of token count', async () => {
684
+ mockGetHistory.mockReturnValue([
685
+ { role: 'user', parts: [{ text: '...history...' }] },
686
+ ]);
687
+ const originalTokenCount = 10; // Well below threshold
688
+ const newTokenCount = 5;
689
+ mockCountTokens
690
+ .mockResolvedValueOnce({ totalTokens: originalTokenCount })
691
+ .mockResolvedValueOnce({ totalTokens: newTokenCount });
692
+ // Mock the summary response from the chat
693
+ mockSendMessage.mockResolvedValue({
694
+ role: 'model',
695
+ parts: [{ text: 'This is a summary.' }],
696
+ });
697
+ const initialChat = client.getChat();
698
+ const result = await client.tryCompressChat('prompt-id-1', true); // force = true
699
+ const newChat = client.getChat();
700
+ expect(mockSendMessage).toHaveBeenCalled();
701
+ expect(result).toEqual({
702
+ compressionStatus: CompressionStatus.COMPRESSED,
703
+ originalTokenCount,
704
+ newTokenCount,
705
+ });
706
+ // Assert that the chat was reset
707
+ expect(newChat).not.toBe(initialChat);
708
+ });
709
+ it('should use current model from config for token counting after sendMessage', async () => {
710
+ const initialModel = client['config'].getModel();
711
+ const mockCountTokens = vi
712
+ .fn()
713
+ .mockResolvedValueOnce({ totalTokens: 100000 })
714
+ .mockResolvedValueOnce({ totalTokens: 5000 });
715
+ const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
716
+ const mockChatHistory = [
717
+ { role: 'user', parts: [{ text: 'Long conversation' }] },
718
+ { role: 'model', parts: [{ text: 'Long response' }] },
719
+ ];
720
+ const mockChat = {
721
+ getHistory: vi.fn().mockReturnValue(mockChatHistory),
722
+ setHistory: vi.fn(),
723
+ sendMessage: mockSendMessage,
724
+ };
725
+ const mockGenerator = {
726
+ countTokens: mockCountTokens,
727
+ };
728
+ // mock the model has been changed between calls of `countTokens`
729
+ const firstCurrentModel = initialModel + '-changed-1';
730
+ const secondCurrentModel = initialModel + '-changed-2';
731
+ vi.spyOn(client['config'], 'getModel')
732
+ .mockReturnValueOnce(firstCurrentModel)
733
+ .mockReturnValueOnce(secondCurrentModel);
734
+ client['chat'] = mockChat;
735
+ client['contentGenerator'] = mockGenerator;
736
+ client['startChat'] = vi.fn().mockResolvedValue(mockChat);
737
+ const result = await client.tryCompressChat('prompt-id-4', true);
738
+ expect(mockCountTokens).toHaveBeenCalledTimes(2);
739
+ expect(mockCountTokens).toHaveBeenNthCalledWith(1, {
740
+ model: firstCurrentModel,
741
+ contents: mockChatHistory,
742
+ });
743
+ expect(mockCountTokens).toHaveBeenNthCalledWith(2, {
744
+ model: secondCurrentModel,
745
+ contents: expect.any(Array),
746
+ });
747
+ expect(result).toEqual({
748
+ compressionStatus: CompressionStatus.COMPRESSED,
749
+ originalTokenCount: 100000,
750
+ newTokenCount: 5000,
751
+ });
752
+ });
753
+ });
754
+ describe('sendMessageStream', () => {
755
+ it('injects a plan mode reminder before user queries when approval mode is PLAN', async () => {
756
+ const mockStream = (async function* () { })();
757
+ mockTurnRunFn.mockReturnValue(mockStream);
758
+ mockConfigObject.getApprovalMode.mockReturnValue(ApprovalModeEnum.PLAN);
759
+ const mockChat = {
760
+ addHistory: vi.fn(),
761
+ getHistory: vi.fn().mockReturnValue([]),
762
+ };
763
+ client['chat'] = mockChat;
764
+ const mockGenerator = {
765
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
766
+ generateContent: mockGenerateContentFn,
767
+ };
768
+ client['contentGenerator'] = mockGenerator;
769
+ const stream = client.sendMessageStream('Plan mode test', new AbortController().signal, 'prompt-plan-1');
770
+ await fromAsync(stream);
771
+ expect(mockTurnRunFn).toHaveBeenCalledWith([getPlanModeSystemReminder(), 'Plan mode test'], expect.any(Object));
772
+ mockConfigObject.getApprovalMode.mockReturnValue(ApprovalModeEnum.DEFAULT);
773
+ });
774
+ it('emits a compression event when the context was automatically compressed', async () => {
775
+ // Arrange
776
+ const mockStream = (async function* () {
777
+ yield { type: 'content', value: 'Hello' };
778
+ })();
779
+ mockTurnRunFn.mockReturnValue(mockStream);
780
+ const mockChat = {
781
+ addHistory: vi.fn(),
782
+ getHistory: vi.fn().mockReturnValue([]),
783
+ };
784
+ client['chat'] = mockChat;
785
+ const mockGenerator = {
786
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
787
+ generateContent: mockGenerateContentFn,
788
+ };
789
+ client['contentGenerator'] = mockGenerator;
790
+ const compressionInfo = {
791
+ compressionStatus: CompressionStatus.COMPRESSED,
792
+ originalTokenCount: 1000,
793
+ newTokenCount: 500,
794
+ };
795
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValueOnce(compressionInfo);
796
+ // Act
797
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
798
+ const events = await fromAsync(stream);
799
+ // Assert
800
+ expect(events).toContainEqual({
801
+ type: GeminiEventType.ChatCompressed,
802
+ value: compressionInfo,
803
+ });
804
+ });
805
+ it.each([
806
+ {
807
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
808
+ },
809
+ { compressionStatus: CompressionStatus.NOOP },
810
+ {
811
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
812
+ },
813
+ ])('does not emit a compression event when the status is $compressionStatus', async ({ compressionStatus }) => {
814
+ // Arrange
815
+ const mockStream = (async function* () {
816
+ yield { type: 'content', value: 'Hello' };
817
+ })();
818
+ mockTurnRunFn.mockReturnValue(mockStream);
819
+ const mockChat = {
820
+ addHistory: vi.fn(),
821
+ getHistory: vi.fn().mockReturnValue([]),
822
+ };
823
+ client['chat'] = mockChat;
824
+ const mockGenerator = {
825
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
826
+ generateContent: mockGenerateContentFn,
827
+ };
828
+ client['contentGenerator'] = mockGenerator;
829
+ const compressionInfo = {
830
+ compressionStatus,
831
+ originalTokenCount: 1000,
832
+ newTokenCount: 500,
833
+ };
834
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValueOnce(compressionInfo);
835
+ // Act
836
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
837
+ const events = await fromAsync(stream);
838
+ // Assert
839
+ expect(events).not.toContainEqual({
840
+ type: GeminiEventType.ChatCompressed,
841
+ value: expect.anything(),
842
+ });
843
+ });
844
+ it('should include editor context when ideMode is enabled', async () => {
845
+ // Arrange
846
+ vi.mocked(ideContext.getIdeContext).mockReturnValue({
847
+ workspaceState: {
848
+ openFiles: [
849
+ {
850
+ path: '/path/to/active/file.ts',
851
+ timestamp: Date.now(),
852
+ isActive: true,
853
+ selectedText: 'hello',
854
+ cursor: { line: 5, character: 10 },
855
+ },
856
+ {
857
+ path: '/path/to/recent/file1.ts',
858
+ timestamp: Date.now(),
859
+ },
860
+ {
861
+ path: '/path/to/recent/file2.ts',
862
+ timestamp: Date.now(),
863
+ },
864
+ ],
865
+ },
866
+ });
867
+ vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
868
+ const mockStream = (async function* () {
869
+ yield { type: 'content', value: 'Hello' };
870
+ })();
871
+ mockTurnRunFn.mockReturnValue(mockStream);
872
+ const mockChat = {
873
+ addHistory: vi.fn(),
874
+ getHistory: vi.fn().mockReturnValue([]),
875
+ };
876
+ client['chat'] = mockChat;
877
+ const mockGenerator = {
878
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
879
+ generateContent: mockGenerateContentFn,
880
+ };
881
+ client['contentGenerator'] = mockGenerator;
882
+ const initialRequest = [{ text: 'Hi' }];
883
+ // Act
884
+ const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
885
+ for await (const _ of stream) {
886
+ // consume stream
887
+ }
888
+ // Assert
889
+ expect(ideContext.getIdeContext).toHaveBeenCalled();
890
+ const expectedContext = `
891
+ Here is the user's editor context as a JSON object. This is for your information only.
892
+ \`\`\`json
893
+ ${JSON.stringify({
894
+ activeFile: {
895
+ path: '/path/to/active/file.ts',
896
+ cursor: {
897
+ line: 5,
898
+ character: 10,
899
+ },
900
+ selectedText: 'hello',
901
+ },
902
+ otherOpenFiles: ['/path/to/recent/file1.ts', '/path/to/recent/file2.ts'],
903
+ }, null, 2)}
904
+ \`\`\`
905
+ `.trim();
906
+ const expectedRequest = [{ text: expectedContext }];
907
+ expect(mockChat.addHistory).toHaveBeenCalledWith({
908
+ role: 'user',
909
+ parts: expectedRequest,
910
+ });
911
+ });
912
+ it('should not add context if ideMode is enabled but no open files', async () => {
913
+ // Arrange
914
+ vi.mocked(ideContext.getIdeContext).mockReturnValue({
915
+ workspaceState: {
916
+ openFiles: [],
917
+ },
918
+ });
919
+ vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
920
+ const mockStream = (async function* () {
921
+ yield { type: 'content', value: 'Hello' };
922
+ })();
923
+ mockTurnRunFn.mockReturnValue(mockStream);
924
+ const mockChat = {
925
+ addHistory: vi.fn(),
926
+ getHistory: vi.fn().mockReturnValue([]),
927
+ };
928
+ client['chat'] = mockChat;
929
+ const mockGenerator = {
930
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
931
+ generateContent: mockGenerateContentFn,
932
+ };
933
+ client['contentGenerator'] = mockGenerator;
934
+ const initialRequest = [{ text: 'Hi' }];
935
+ // Act
936
+ const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
937
+ for await (const _ of stream) {
938
+ // consume stream
939
+ }
940
+ // Assert
941
+ expect(ideContext.getIdeContext).toHaveBeenCalled();
942
+ expect(mockTurnRunFn).toHaveBeenCalledWith(['Hi'], expect.any(Object));
943
+ });
944
+ it('should add context if ideMode is enabled and there is one active file', async () => {
945
+ // Arrange
946
+ vi.mocked(ideContext.getIdeContext).mockReturnValue({
947
+ workspaceState: {
948
+ openFiles: [
949
+ {
950
+ path: '/path/to/active/file.ts',
951
+ timestamp: Date.now(),
952
+ isActive: true,
953
+ selectedText: 'hello',
954
+ cursor: { line: 5, character: 10 },
955
+ },
956
+ ],
957
+ },
958
+ });
959
+ vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
960
+ const mockStream = (async function* () {
961
+ yield { type: 'content', value: 'Hello' };
962
+ })();
963
+ mockTurnRunFn.mockReturnValue(mockStream);
964
+ const mockChat = {
965
+ addHistory: vi.fn(),
966
+ getHistory: vi.fn().mockReturnValue([]),
967
+ };
968
+ client['chat'] = mockChat;
969
+ const mockGenerator = {
970
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
971
+ generateContent: mockGenerateContentFn,
972
+ };
973
+ client['contentGenerator'] = mockGenerator;
974
+ const initialRequest = [{ text: 'Hi' }];
975
+ // Act
976
+ const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
977
+ for await (const _ of stream) {
978
+ // consume stream
979
+ }
980
+ // Assert
981
+ expect(ideContext.getIdeContext).toHaveBeenCalled();
982
+ const expectedContext = `
983
+ Here is the user's editor context as a JSON object. This is for your information only.
984
+ \`\`\`json
985
+ ${JSON.stringify({
986
+ activeFile: {
987
+ path: '/path/to/active/file.ts',
988
+ cursor: {
989
+ line: 5,
990
+ character: 10,
991
+ },
992
+ selectedText: 'hello',
993
+ },
994
+ }, null, 2)}
995
+ \`\`\`
996
+ `.trim();
997
+ const expectedRequest = [{ text: expectedContext }];
998
+ expect(mockChat.addHistory).toHaveBeenCalledWith({
999
+ role: 'user',
1000
+ parts: expectedRequest,
1001
+ });
1002
+ });
1003
+ it('should add context if ideMode is enabled and there are open files but no active file', async () => {
1004
+ // Arrange
1005
+ vi.mocked(ideContext.getIdeContext).mockReturnValue({
1006
+ workspaceState: {
1007
+ openFiles: [
1008
+ {
1009
+ path: '/path/to/recent/file1.ts',
1010
+ timestamp: Date.now(),
1011
+ },
1012
+ {
1013
+ path: '/path/to/recent/file2.ts',
1014
+ timestamp: Date.now(),
1015
+ },
1016
+ ],
1017
+ },
1018
+ });
1019
+ vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1020
+ const mockStream = (async function* () {
1021
+ yield { type: 'content', value: 'Hello' };
1022
+ })();
1023
+ mockTurnRunFn.mockReturnValue(mockStream);
1024
+ const mockChat = {
1025
+ addHistory: vi.fn(),
1026
+ getHistory: vi.fn().mockReturnValue([]),
1027
+ };
1028
+ client['chat'] = mockChat;
1029
+ const mockGenerator = {
1030
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1031
+ generateContent: mockGenerateContentFn,
1032
+ };
1033
+ client['contentGenerator'] = mockGenerator;
1034
+ const initialRequest = [{ text: 'Hi' }];
1035
+ // Act
1036
+ const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
1037
+ for await (const _ of stream) {
1038
+ // consume stream
1039
+ }
1040
+ // Assert
1041
+ expect(ideContext.getIdeContext).toHaveBeenCalled();
1042
+ const expectedContext = `
1043
+ Here is the user's editor context as a JSON object. This is for your information only.
1044
+ \`\`\`json
1045
+ ${JSON.stringify({
1046
+ otherOpenFiles: ['/path/to/recent/file1.ts', '/path/to/recent/file2.ts'],
1047
+ }, null, 2)}
1048
+ \`\`\`
1049
+ `.trim();
1050
+ const expectedRequest = [{ text: expectedContext }];
1051
+ expect(mockChat.addHistory).toHaveBeenCalledWith({
1052
+ role: 'user',
1053
+ parts: expectedRequest,
1054
+ });
1055
+ });
1056
+ it('should return the turn instance after the stream is complete', async () => {
1057
+ // Arrange
1058
+ const mockStream = (async function* () {
1059
+ yield { type: 'content', value: 'Hello' };
1060
+ })();
1061
+ mockTurnRunFn.mockReturnValue(mockStream);
1062
+ const mockChat = {
1063
+ addHistory: vi.fn(),
1064
+ getHistory: vi.fn().mockReturnValue([]),
1065
+ };
1066
+ client['chat'] = mockChat;
1067
+ const mockGenerator = {
1068
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1069
+ generateContent: mockGenerateContentFn,
1070
+ };
1071
+ client['contentGenerator'] = mockGenerator;
1072
+ // Act
1073
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
1074
+ // Consume the stream manually to get the final return value.
1075
+ let finalResult;
1076
+ while (true) {
1077
+ const result = await stream.next();
1078
+ if (result.done) {
1079
+ finalResult = result.value;
1080
+ break;
1081
+ }
1082
+ }
1083
+ // Assert
1084
+ expect(finalResult).toBeInstanceOf(Turn);
1085
+ });
1086
+ it('should stop infinite loop after MAX_TURNS when nextSpeaker always returns model', async () => {
1087
+ // Get the mocked checkNextSpeaker function and configure it to trigger infinite loop
1088
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
1089
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
1090
+ mockCheckNextSpeaker.mockResolvedValue({
1091
+ next_speaker: 'model',
1092
+ reasoning: 'Test case - always continue',
1093
+ });
1094
+ // Mock Turn to have no pending tool calls (which would allow nextSpeaker check)
1095
+ const mockStream = (async function* () {
1096
+ yield { type: 'content', value: 'Continue...' };
1097
+ })();
1098
+ mockTurnRunFn.mockReturnValue(mockStream);
1099
+ const mockChat = {
1100
+ addHistory: vi.fn(),
1101
+ getHistory: vi.fn().mockReturnValue([]),
1102
+ };
1103
+ client['chat'] = mockChat;
1104
+ const mockGenerator = {
1105
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1106
+ generateContent: mockGenerateContentFn,
1107
+ };
1108
+ client['contentGenerator'] = mockGenerator;
1109
+ // Use a signal that never gets aborted
1110
+ const abortController = new AbortController();
1111
+ const signal = abortController.signal;
1112
+ // Act - Start the stream that should loop
1113
+ const stream = client.sendMessageStream([{ text: 'Start conversation' }], signal, 'prompt-id-2');
1114
+ // Count how many stream events we get
1115
+ let eventCount = 0;
1116
+ let finalResult;
1117
+ // Consume the stream and count iterations
1118
+ while (true) {
1119
+ const result = await stream.next();
1120
+ if (result.done) {
1121
+ finalResult = result.value;
1122
+ break;
1123
+ }
1124
+ eventCount++;
1125
+ // Safety check to prevent actual infinite loop in test
1126
+ if (eventCount > 200) {
1127
+ abortController.abort();
1128
+ throw new Error('Test exceeded expected event limit - possible actual infinite loop');
1129
+ }
1130
+ }
1131
+ // Assert
1132
+ expect(finalResult).toBeInstanceOf(Turn);
1133
+ // Debug: Check how many times checkNextSpeaker was called
1134
+ const callCount = mockCheckNextSpeaker.mock.calls.length;
1135
+ // If infinite loop protection is working, checkNextSpeaker should be called many times
1136
+ // but stop at MAX_TURNS (100). Since each recursive call should trigger checkNextSpeaker,
1137
+ // we expect it to be called multiple times before hitting the limit
1138
+ expect(mockCheckNextSpeaker).toHaveBeenCalled();
1139
+ // The test should demonstrate that the infinite loop protection works:
1140
+ // - If checkNextSpeaker is called many times (close to MAX_TURNS), it shows the loop was happening
1141
+ // - If it's only called once, the recursive behavior might not be triggered
1142
+ if (callCount === 0) {
1143
+ throw new Error('checkNextSpeaker was never called - the recursive condition was not met');
1144
+ }
1145
+ else if (callCount === 1) {
1146
+ // This might be expected behavior if the turn has pending tool calls or other conditions prevent recursion
1147
+ console.log('checkNextSpeaker called only once - no infinite loop occurred');
1148
+ }
1149
+ else {
1150
+ console.log(`checkNextSpeaker called ${callCount} times - infinite loop protection worked`);
1151
+ // If called multiple times, we expect it to be stopped before MAX_TURNS
1152
+ expect(callCount).toBeLessThanOrEqual(100); // Should not exceed MAX_TURNS
1153
+ }
1154
+ // The stream should produce events and eventually terminate
1155
+ expect(eventCount).toBeGreaterThanOrEqual(1);
1156
+ expect(eventCount).toBeLessThan(200); // Should not exceed our safety limit
1157
+ });
1158
+ it('should yield MaxSessionTurns and stop when session turn limit is reached', async () => {
1159
+ // Arrange
1160
+ const MAX_SESSION_TURNS = 5;
1161
+ vi.spyOn(client['config'], 'getMaxSessionTurns').mockReturnValue(MAX_SESSION_TURNS);
1162
+ const mockStream = (async function* () {
1163
+ yield { type: 'content', value: 'Hello' };
1164
+ })();
1165
+ mockTurnRunFn.mockReturnValue(mockStream);
1166
+ const mockChat = {
1167
+ addHistory: vi.fn(),
1168
+ getHistory: vi.fn().mockReturnValue([]),
1169
+ };
1170
+ client['chat'] = mockChat;
1171
+ const mockGenerator = {
1172
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1173
+ generateContent: mockGenerateContentFn,
1174
+ };
1175
+ client['contentGenerator'] = mockGenerator;
1176
+ // Act & Assert
1177
+ // Run up to the limit
1178
+ for (let i = 0; i < MAX_SESSION_TURNS; i++) {
1179
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-4');
1180
+ // consume stream
1181
+ for await (const _event of stream) {
1182
+ // do nothing
1183
+ }
1184
+ }
1185
+ // This call should exceed the limit
1186
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-5');
1187
+ const events = [];
1188
+ for await (const event of stream) {
1189
+ events.push(event);
1190
+ }
1191
+ expect(events).toEqual([{ type: GeminiEventType.MaxSessionTurns }]);
1192
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(MAX_SESSION_TURNS);
1193
+ });
1194
+ it('should respect MAX_TURNS limit even when turns parameter is set to a large value', async () => {
1195
+ // This test verifies that the infinite loop protection works even when
1196
+ // someone tries to bypass it by calling with a very large turns value
1197
+ // Get the mocked checkNextSpeaker function and configure it to trigger infinite loop
1198
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
1199
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
1200
+ mockCheckNextSpeaker.mockResolvedValue({
1201
+ next_speaker: 'model',
1202
+ reasoning: 'Test case - always continue',
1203
+ });
1204
+ // Mock Turn to have no pending tool calls (which would allow nextSpeaker check)
1205
+ const mockStream = (async function* () {
1206
+ yield { type: 'content', value: 'Continue...' };
1207
+ })();
1208
+ mockTurnRunFn.mockReturnValue(mockStream);
1209
+ const mockChat = {
1210
+ addHistory: vi.fn(),
1211
+ getHistory: vi.fn().mockReturnValue([]),
1212
+ };
1213
+ client['chat'] = mockChat;
1214
+ const mockGenerator = {
1215
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1216
+ generateContent: mockGenerateContentFn,
1217
+ };
1218
+ client['contentGenerator'] = mockGenerator;
1219
+ // Use a signal that never gets aborted
1220
+ const abortController = new AbortController();
1221
+ const signal = abortController.signal;
1222
+ // Act - Start the stream with an extremely high turns value
1223
+ // This simulates a case where the turns protection is bypassed
1224
+ const stream = client.sendMessageStream([{ text: 'Start conversation' }], signal, 'prompt-id-3', Number.MAX_SAFE_INTEGER);
1225
+ // Count how many stream events we get
1226
+ let eventCount = 0;
1227
+ const maxTestIterations = 1000; // Higher limit to show the loop continues
1228
+ // Consume the stream and count iterations
1229
+ try {
1230
+ while (true) {
1231
+ const result = await stream.next();
1232
+ if (result.done) {
1233
+ break;
1234
+ }
1235
+ eventCount++;
1236
+ // This test should hit this limit, demonstrating the infinite loop
1237
+ if (eventCount > maxTestIterations) {
1238
+ abortController.abort();
1239
+ // This is the expected behavior - we hit the infinite loop
1240
+ break;
1241
+ }
1242
+ }
1243
+ }
1244
+ catch (error) {
1245
+ // If the test framework times out, that also demonstrates the infinite loop
1246
+ console.error('Test timed out or errored:', error);
1247
+ }
1248
+ // Assert that the fix works - the loop should stop at MAX_TURNS
1249
+ const callCount = mockCheckNextSpeaker.mock.calls.length;
1250
+ // With the fix: even when turns is set to a very high value,
1251
+ // the loop should stop at MAX_TURNS (100)
1252
+ expect(callCount).toBeLessThanOrEqual(100); // Should not exceed MAX_TURNS
1253
+ expect(eventCount).toBeLessThanOrEqual(200); // Should have reasonable number of events
1254
+ console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
1255
+ `${eventCount} events generated (properly bounded by MAX_TURNS)`);
1256
+ });
1257
+ describe('Editor context delta', () => {
1258
+ const mockStream = (async function* () {
1259
+ yield { type: 'content', value: 'Hello' };
1260
+ })();
1261
+ beforeEach(() => {
1262
+ client['forceFullIdeContext'] = false; // Reset before each delta test
1263
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1264
+ originalTokenCount: 0,
1265
+ newTokenCount: 0,
1266
+ compressionStatus: CompressionStatus.COMPRESSED,
1267
+ });
1268
+ vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1269
+ mockTurnRunFn.mockReturnValue(mockStream);
1270
+ const mockChat = {
1271
+ addHistory: vi.fn(),
1272
+ setHistory: vi.fn(),
1273
+ sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1274
+ // Assume history is not empty for delta checks
1275
+ getHistory: vi
1276
+ .fn()
1277
+ .mockReturnValue([
1278
+ { role: 'user', parts: [{ text: 'previous message' }] },
1279
+ ]),
1280
+ };
1281
+ client['chat'] = mockChat;
1282
+ const mockGenerator = {
1283
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1284
+ generateContent: mockGenerateContentFn,
1285
+ };
1286
+ client['contentGenerator'] = mockGenerator;
1287
+ });
1288
+ const testCases = [
1289
+ {
1290
+ description: 'sends delta when active file changes',
1291
+ previousActiveFile: {
1292
+ path: '/path/to/old/file.ts',
1293
+ cursor: { line: 5, character: 10 },
1294
+ selectedText: 'hello',
1295
+ },
1296
+ currentActiveFile: {
1297
+ path: '/path/to/active/file.ts',
1298
+ cursor: { line: 5, character: 10 },
1299
+ selectedText: 'hello',
1300
+ },
1301
+ shouldSendContext: true,
1302
+ },
1303
+ {
1304
+ description: 'sends delta when cursor line changes',
1305
+ previousActiveFile: {
1306
+ path: '/path/to/active/file.ts',
1307
+ cursor: { line: 1, character: 10 },
1308
+ selectedText: 'hello',
1309
+ },
1310
+ currentActiveFile: {
1311
+ path: '/path/to/active/file.ts',
1312
+ cursor: { line: 5, character: 10 },
1313
+ selectedText: 'hello',
1314
+ },
1315
+ shouldSendContext: true,
1316
+ },
1317
+ {
1318
+ description: 'sends delta when cursor character changes',
1319
+ previousActiveFile: {
1320
+ path: '/path/to/active/file.ts',
1321
+ cursor: { line: 5, character: 1 },
1322
+ selectedText: 'hello',
1323
+ },
1324
+ currentActiveFile: {
1325
+ path: '/path/to/active/file.ts',
1326
+ cursor: { line: 5, character: 10 },
1327
+ selectedText: 'hello',
1328
+ },
1329
+ shouldSendContext: true,
1330
+ },
1331
+ {
1332
+ description: 'sends delta when selected text changes',
1333
+ previousActiveFile: {
1334
+ path: '/path/to/active/file.ts',
1335
+ cursor: { line: 5, character: 10 },
1336
+ selectedText: 'world',
1337
+ },
1338
+ currentActiveFile: {
1339
+ path: '/path/to/active/file.ts',
1340
+ cursor: { line: 5, character: 10 },
1341
+ selectedText: 'hello',
1342
+ },
1343
+ shouldSendContext: true,
1344
+ },
1345
+ {
1346
+ description: 'sends delta when selected text is added',
1347
+ previousActiveFile: {
1348
+ path: '/path/to/active/file.ts',
1349
+ cursor: { line: 5, character: 10 },
1350
+ },
1351
+ currentActiveFile: {
1352
+ path: '/path/to/active/file.ts',
1353
+ cursor: { line: 5, character: 10 },
1354
+ selectedText: 'hello',
1355
+ },
1356
+ shouldSendContext: true,
1357
+ },
1358
+ {
1359
+ description: 'sends delta when selected text is removed',
1360
+ previousActiveFile: {
1361
+ path: '/path/to/active/file.ts',
1362
+ cursor: { line: 5, character: 10 },
1363
+ selectedText: 'hello',
1364
+ },
1365
+ currentActiveFile: {
1366
+ path: '/path/to/active/file.ts',
1367
+ cursor: { line: 5, character: 10 },
1368
+ },
1369
+ shouldSendContext: true,
1370
+ },
1371
+ {
1372
+ description: 'does not send context when nothing changes',
1373
+ previousActiveFile: {
1374
+ path: '/path/to/active/file.ts',
1375
+ cursor: { line: 5, character: 10 },
1376
+ selectedText: 'hello',
1377
+ },
1378
+ currentActiveFile: {
1379
+ path: '/path/to/active/file.ts',
1380
+ cursor: { line: 5, character: 10 },
1381
+ selectedText: 'hello',
1382
+ },
1383
+ shouldSendContext: false,
1384
+ },
1385
+ ];
1386
+ it.each(testCases)('$description', async ({ previousActiveFile, currentActiveFile, shouldSendContext, }) => {
1387
+ // Setup previous context
1388
+ client['lastSentIdeContext'] = {
1389
+ workspaceState: {
1390
+ openFiles: [
1391
+ {
1392
+ path: previousActiveFile.path,
1393
+ cursor: previousActiveFile.cursor,
1394
+ selectedText: previousActiveFile.selectedText,
1395
+ isActive: true,
1396
+ timestamp: Date.now() - 1000,
1397
+ },
1398
+ ],
1399
+ },
1400
+ };
1401
+ // Setup current context
1402
+ vi.mocked(ideContext.getIdeContext).mockReturnValue({
1403
+ workspaceState: {
1404
+ openFiles: [
1405
+ { ...currentActiveFile, isActive: true, timestamp: Date.now() },
1406
+ ],
1407
+ },
1408
+ });
1409
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-delta');
1410
+ for await (const _ of stream) {
1411
+ // consume stream
1412
+ }
1413
+ const mockChat = client['chat'];
1414
+ if (shouldSendContext) {
1415
+ expect(mockChat.addHistory).toHaveBeenCalledWith(expect.objectContaining({
1416
+ parts: expect.arrayContaining([
1417
+ expect.objectContaining({
1418
+ text: expect.stringContaining("Here is a summary of changes in the user's editor context"),
1419
+ }),
1420
+ ]),
1421
+ }));
1422
+ }
1423
+ else {
1424
+ expect(mockChat.addHistory).not.toHaveBeenCalled();
1425
+ }
1426
+ });
1427
+ it('sends full context when history is cleared, even if editor state is unchanged', async () => {
1428
+ const activeFile = {
1429
+ path: '/path/to/active/file.ts',
1430
+ cursor: { line: 5, character: 10 },
1431
+ selectedText: 'hello',
1432
+ };
1433
+ // Setup previous context
1434
+ client['lastSentIdeContext'] = {
1435
+ workspaceState: {
1436
+ openFiles: [
1437
+ {
1438
+ path: activeFile.path,
1439
+ cursor: activeFile.cursor,
1440
+ selectedText: activeFile.selectedText,
1441
+ isActive: true,
1442
+ timestamp: Date.now() - 1000,
1443
+ },
1444
+ ],
1445
+ },
1446
+ };
1447
+ // Setup current context (same as previous)
1448
+ vi.mocked(ideContext.getIdeContext).mockReturnValue({
1449
+ workspaceState: {
1450
+ openFiles: [
1451
+ { ...activeFile, isActive: true, timestamp: Date.now() },
1452
+ ],
1453
+ },
1454
+ });
1455
+ // Make history empty
1456
+ const mockChat = client['chat'];
1457
+ mockChat.getHistory.mockReturnValue([]);
1458
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-history-cleared');
1459
+ for await (const _ of stream) {
1460
+ // consume stream
1461
+ }
1462
+ expect(mockChat.addHistory).toHaveBeenCalledWith(expect.objectContaining({
1463
+ parts: expect.arrayContaining([
1464
+ expect.objectContaining({
1465
+ text: expect.stringContaining("Here is the user's editor context"),
1466
+ }),
1467
+ ]),
1468
+ }));
1469
+ // Also verify it's the full context, not a delta.
1470
+ const call = mockChat.addHistory.mock.calls[0][0];
1471
+ const contextText = call.parts[0].text;
1472
+ const contextJson = JSON.parse(contextText.match(/```json\n(.*)\n```/s)[1]);
1473
+ expect(contextJson).toHaveProperty('activeFile');
1474
+ expect(contextJson.activeFile.path).toBe('/path/to/active/file.ts');
1475
+ });
1476
+ });
1477
+ describe('IDE context with pending tool calls', () => {
1478
+ let mockChat;
1479
+ beforeEach(() => {
1480
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1481
+ originalTokenCount: 0,
1482
+ newTokenCount: 0,
1483
+ compressionStatus: CompressionStatus.COMPRESSED,
1484
+ });
1485
+ const mockStream = (async function* () {
1486
+ yield { type: 'content', value: 'response' };
1487
+ })();
1488
+ mockTurnRunFn.mockReturnValue(mockStream);
1489
+ mockChat = {
1490
+ addHistory: vi.fn(),
1491
+ getHistory: vi.fn().mockReturnValue([]), // Default empty history
1492
+ setHistory: vi.fn(),
1493
+ sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1494
+ };
1495
+ client['chat'] = mockChat;
1496
+ const mockGenerator = {
1497
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1498
+ };
1499
+ client['contentGenerator'] = mockGenerator;
1500
+ vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1501
+ vi.mocked(ideContext.getIdeContext).mockReturnValue({
1502
+ workspaceState: {
1503
+ openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
1504
+ },
1505
+ });
1506
+ });
1507
+ it('should NOT add IDE context when a tool call is pending', async () => {
1508
+ // Arrange: History ends with a functionCall from the model
1509
+ const historyWithPendingCall = [
1510
+ { role: 'user', parts: [{ text: 'Please use a tool.' }] },
1511
+ {
1512
+ role: 'model',
1513
+ parts: [{ functionCall: { name: 'some_tool', args: {} } }],
1514
+ },
1515
+ ];
1516
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyWithPendingCall);
1517
+ // Act: Simulate sending the tool's response back
1518
+ const stream = client.sendMessageStream([
1519
+ {
1520
+ functionResponse: {
1521
+ name: 'some_tool',
1522
+ response: { success: true },
1523
+ },
1524
+ },
1525
+ ], new AbortController().signal, 'prompt-id-tool-response');
1526
+ for await (const _ of stream) {
1527
+ // consume stream to complete the call
1528
+ }
1529
+ // Assert: The IDE context message should NOT have been added to the history.
1530
+ expect(mockChat.addHistory).not.toHaveBeenCalledWith(expect.objectContaining({
1531
+ parts: expect.arrayContaining([
1532
+ expect.objectContaining({
1533
+ text: expect.stringContaining("user's editor context"),
1534
+ }),
1535
+ ]),
1536
+ }));
1537
+ });
1538
+ it('should add IDE context when no tool call is pending', async () => {
1539
+ // Arrange: History is normal, no pending calls
1540
+ const normalHistory = [
1541
+ { role: 'user', parts: [{ text: 'A normal message.' }] },
1542
+ { role: 'model', parts: [{ text: 'A normal response.' }] },
1543
+ ];
1544
+ vi.mocked(mockChat.getHistory).mockReturnValue(normalHistory);
1545
+ // Act
1546
+ const stream = client.sendMessageStream([{ text: 'Another normal message' }], new AbortController().signal, 'prompt-id-normal');
1547
+ for await (const _ of stream) {
1548
+ // consume stream
1549
+ }
1550
+ // Assert: The IDE context message SHOULD have been added.
1551
+ expect(mockChat.addHistory).toHaveBeenCalledWith(expect.objectContaining({
1552
+ role: 'user',
1553
+ parts: expect.arrayContaining([
1554
+ expect.objectContaining({
1555
+ text: expect.stringContaining("user's editor context"),
1556
+ }),
1557
+ ]),
1558
+ }));
1559
+ });
1560
+ it('should send the latest IDE context on the next message after a skipped context', async () => {
1561
+ // --- Step 1: A tool call is pending, context should be skipped ---
1562
+ // Arrange: History ends with a functionCall
1563
+ const historyWithPendingCall = [
1564
+ { role: 'user', parts: [{ text: 'Please use a tool.' }] },
1565
+ {
1566
+ role: 'model',
1567
+ parts: [{ functionCall: { name: 'some_tool', args: {} } }],
1568
+ },
1569
+ ];
1570
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyWithPendingCall);
1571
+ // Arrange: Set the initial IDE context
1572
+ const initialIdeContext = {
1573
+ workspaceState: {
1574
+ openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
1575
+ },
1576
+ };
1577
+ vi.mocked(ideContext.getIdeContext).mockReturnValue(initialIdeContext);
1578
+ // Act: Send the tool response
1579
+ let stream = client.sendMessageStream([
1580
+ {
1581
+ functionResponse: {
1582
+ name: 'some_tool',
1583
+ response: { success: true },
1584
+ },
1585
+ },
1586
+ ], new AbortController().signal, 'prompt-id-tool-response');
1587
+ for await (const _ of stream) {
1588
+ /* consume */
1589
+ }
1590
+ // Assert: The initial context was NOT sent
1591
+ expect(mockChat.addHistory).not.toHaveBeenCalledWith(expect.objectContaining({
1592
+ parts: expect.arrayContaining([
1593
+ expect.objectContaining({
1594
+ text: expect.stringContaining("user's editor context"),
1595
+ }),
1596
+ ]),
1597
+ }));
1598
+ // --- Step 2: A new message is sent, latest context should be included ---
1599
+ // Arrange: The model has responded to the tool, and the user is sending a new message.
1600
+ const historyAfterToolResponse = [
1601
+ ...historyWithPendingCall,
1602
+ {
1603
+ role: 'user',
1604
+ parts: [
1605
+ {
1606
+ functionResponse: {
1607
+ name: 'some_tool',
1608
+ response: { success: true },
1609
+ },
1610
+ },
1611
+ ],
1612
+ },
1613
+ { role: 'model', parts: [{ text: 'The tool ran successfully.' }] },
1614
+ ];
1615
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyAfterToolResponse);
1616
+ vi.mocked(mockChat.addHistory).mockClear(); // Clear previous calls for the next assertion
1617
+ // Arrange: The IDE context has now changed
1618
+ const newIdeContext = {
1619
+ workspaceState: {
1620
+ openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
1621
+ },
1622
+ };
1623
+ vi.mocked(ideContext.getIdeContext).mockReturnValue(newIdeContext);
1624
+ // Act: Send a new, regular user message
1625
+ stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1626
+ for await (const _ of stream) {
1627
+ /* consume */
1628
+ }
1629
+ // Assert: The NEW context was sent as a FULL context because there was no previously sent context.
1630
+ const addHistoryCalls = vi.mocked(mockChat.addHistory).mock.calls;
1631
+ const contextCall = addHistoryCalls.find((call) => JSON.stringify(call[0]).includes("user's editor context"));
1632
+ expect(contextCall).toBeDefined();
1633
+ expect(JSON.stringify(contextCall[0])).toContain("Here is the user's editor context as a JSON object");
1634
+ // Check that the sent context is the new one (fileB.ts)
1635
+ expect(JSON.stringify(contextCall[0])).toContain('fileB.ts');
1636
+ // Check that the sent context is NOT the old one (fileA.ts)
1637
+ expect(JSON.stringify(contextCall[0])).not.toContain('fileA.ts');
1638
+ });
1639
+ it('should send a context DELTA on the next message after a skipped context', async () => {
1640
+ // --- Step 0: Establish an initial context ---
1641
+ vi.mocked(mockChat.getHistory).mockReturnValue([]); // Start with empty history
1642
+ const contextA = {
1643
+ workspaceState: {
1644
+ openFiles: [
1645
+ {
1646
+ path: '/path/to/fileA.ts',
1647
+ isActive: true,
1648
+ timestamp: Date.now(),
1649
+ },
1650
+ ],
1651
+ },
1652
+ };
1653
+ vi.mocked(ideContext.getIdeContext).mockReturnValue(contextA);
1654
+ // Act: Send a regular message to establish the initial context
1655
+ let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
1656
+ for await (const _ of stream) {
1657
+ /* consume */
1658
+ }
1659
+ // Assert: Full context for fileA.ts was sent and stored.
1660
+ const initialCall = vi.mocked(mockChat.addHistory).mock.calls[0][0];
1661
+ expect(JSON.stringify(initialCall)).toContain("user's editor context as a JSON object");
1662
+ expect(JSON.stringify(initialCall)).toContain('fileA.ts');
1663
+ // This implicitly tests that `lastSentIdeContext` is now set internally by the client.
1664
+ vi.mocked(mockChat.addHistory).mockClear();
1665
+ // --- Step 1: A tool call is pending, context should be skipped ---
1666
+ const historyWithPendingCall = [
1667
+ { role: 'user', parts: [{ text: 'Please use a tool.' }] },
1668
+ {
1669
+ role: 'model',
1670
+ parts: [{ functionCall: { name: 'some_tool', args: {} } }],
1671
+ },
1672
+ ];
1673
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyWithPendingCall);
1674
+ // Arrange: IDE context changes, but this should be skipped
1675
+ const contextB = {
1676
+ workspaceState: {
1677
+ openFiles: [
1678
+ {
1679
+ path: '/path/to/fileB.ts',
1680
+ isActive: true,
1681
+ timestamp: Date.now(),
1682
+ },
1683
+ ],
1684
+ },
1685
+ };
1686
+ vi.mocked(ideContext.getIdeContext).mockReturnValue(contextB);
1687
+ // Act: Send the tool response
1688
+ stream = client.sendMessageStream([
1689
+ {
1690
+ functionResponse: {
1691
+ name: 'some_tool',
1692
+ response: { success: true },
1693
+ },
1694
+ },
1695
+ ], new AbortController().signal, 'prompt-id-tool-response');
1696
+ for await (const _ of stream) {
1697
+ /* consume */
1698
+ }
1699
+ // Assert: No context was sent
1700
+ expect(mockChat.addHistory).not.toHaveBeenCalled();
1701
+ // --- Step 2: A new message is sent, latest context DELTA should be included ---
1702
+ const historyAfterToolResponse = [
1703
+ ...historyWithPendingCall,
1704
+ {
1705
+ role: 'user',
1706
+ parts: [
1707
+ {
1708
+ functionResponse: {
1709
+ name: 'some_tool',
1710
+ response: { success: true },
1711
+ },
1712
+ },
1713
+ ],
1714
+ },
1715
+ { role: 'model', parts: [{ text: 'The tool ran successfully.' }] },
1716
+ ];
1717
+ vi.mocked(mockChat.getHistory).mockReturnValue(historyAfterToolResponse);
1718
+ // Arrange: The IDE context has changed again
1719
+ const contextC = {
1720
+ workspaceState: {
1721
+ openFiles: [
1722
+ // fileA is now closed, fileC is open
1723
+ {
1724
+ path: '/path/to/fileC.ts',
1725
+ isActive: true,
1726
+ timestamp: Date.now(),
1727
+ },
1728
+ ],
1729
+ },
1730
+ };
1731
+ vi.mocked(ideContext.getIdeContext).mockReturnValue(contextC);
1732
+ // Act: Send a new, regular user message
1733
+ stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1734
+ for await (const _ of stream) {
1735
+ /* consume */
1736
+ }
1737
+ // Assert: The DELTA context was sent
1738
+ const finalCall = vi.mocked(mockChat.addHistory).mock.calls[0][0];
1739
+ expect(JSON.stringify(finalCall)).toContain('summary of changes');
1740
+ // The delta should reflect fileA being closed and fileC being opened.
1741
+ expect(JSON.stringify(finalCall)).toContain('filesClosed');
1742
+ expect(JSON.stringify(finalCall)).toContain('fileA.ts');
1743
+ expect(JSON.stringify(finalCall)).toContain('activeFileChanged');
1744
+ expect(JSON.stringify(finalCall)).toContain('fileC.ts');
1745
+ });
1746
+ });
1747
+ it('should not call checkNextSpeaker when turn.run() yields an error', async () => {
1748
+ // Arrange
1749
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
1750
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
1751
+ const mockStream = (async function* () {
1752
+ yield {
1753
+ type: GeminiEventType.Error,
1754
+ value: { error: { message: 'test error' } },
1755
+ };
1756
+ })();
1757
+ mockTurnRunFn.mockReturnValue(mockStream);
1758
+ const mockChat = {
1759
+ addHistory: vi.fn(),
1760
+ getHistory: vi.fn().mockReturnValue([]),
1761
+ };
1762
+ client['chat'] = mockChat;
1763
+ const mockGenerator = {
1764
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1765
+ generateContent: mockGenerateContentFn,
1766
+ };
1767
+ client['contentGenerator'] = mockGenerator;
1768
+ // Act
1769
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1770
+ for await (const _ of stream) {
1771
+ // consume stream
1772
+ }
1773
+ // Assert
1774
+ expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
1775
+ });
1776
+ it('should not call checkNextSpeaker when turn.run() yields a value then an error', async () => {
1777
+ // Arrange
1778
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
1779
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
1780
+ const mockStream = (async function* () {
1781
+ yield { type: GeminiEventType.Content, value: 'some content' };
1782
+ yield {
1783
+ type: GeminiEventType.Error,
1784
+ value: { error: { message: 'test error' } },
1785
+ };
1786
+ })();
1787
+ mockTurnRunFn.mockReturnValue(mockStream);
1788
+ const mockChat = {
1789
+ addHistory: vi.fn(),
1790
+ getHistory: vi.fn().mockReturnValue([]),
1791
+ };
1792
+ client['chat'] = mockChat;
1793
+ const mockGenerator = {
1794
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1795
+ generateContent: mockGenerateContentFn,
1796
+ };
1797
+ client['contentGenerator'] = mockGenerator;
1798
+ // Act
1799
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1800
+ for await (const _ of stream) {
1801
+ // consume stream
1802
+ }
1803
+ // Assert
1804
+ expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
1805
+ });
1806
+ it('does not run loop checks when skipLoopDetection is true', async () => {
1807
+ // Arrange
1808
+ // Ensure config returns true for skipLoopDetection
1809
+ vi.spyOn(client['config'], 'getSkipLoopDetection').mockReturnValue(true);
1810
+ // Replace loop detector with spies
1811
+ const ldMock = {
1812
+ turnStarted: vi.fn().mockResolvedValue(false),
1813
+ addAndCheck: vi.fn().mockReturnValue(false),
1814
+ reset: vi.fn(),
1815
+ };
1816
+ // @ts-expect-error override private for testing
1817
+ client['loopDetector'] = ldMock;
1818
+ const mockStream = (async function* () {
1819
+ yield { type: 'content', value: 'Hello' };
1820
+ yield { type: 'content', value: 'World' };
1821
+ })();
1822
+ mockTurnRunFn.mockReturnValue(mockStream);
1823
+ const mockChat = {
1824
+ addHistory: vi.fn(),
1825
+ getHistory: vi.fn().mockReturnValue([]),
1826
+ };
1827
+ client['chat'] = mockChat;
1828
+ const mockGenerator = {
1829
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1830
+ generateContent: mockGenerateContentFn,
1831
+ };
1832
+ client['contentGenerator'] = mockGenerator;
1833
+ // Act
1834
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop-skip');
1835
+ for await (const _ of stream) {
1836
+ // consume
1837
+ }
1838
+ // Assert: methods not called due to skip
1839
+ const ld = client['loopDetector'];
1840
+ expect(ld.turnStarted).not.toHaveBeenCalled();
1841
+ expect(ld.addAndCheck).not.toHaveBeenCalled();
1842
+ });
1843
+ it('runs loop checks when skipLoopDetection is false', async () => {
1844
+ // Arrange
1845
+ vi.spyOn(client['config'], 'getSkipLoopDetection').mockReturnValue(false);
1846
+ const turnStarted = vi.fn().mockResolvedValue(false);
1847
+ const addAndCheck = vi.fn().mockReturnValue(false);
1848
+ const reset = vi.fn();
1849
+ // @ts-expect-error override private for testing
1850
+ client['loopDetector'] = { turnStarted, addAndCheck, reset };
1851
+ const mockStream = (async function* () {
1852
+ yield { type: 'content', value: 'Hello' };
1853
+ yield { type: 'content', value: 'World' };
1854
+ })();
1855
+ mockTurnRunFn.mockReturnValue(mockStream);
1856
+ const mockChat = {
1857
+ addHistory: vi.fn(),
1858
+ getHistory: vi.fn().mockReturnValue([]),
1859
+ };
1860
+ client['chat'] = mockChat;
1861
+ const mockGenerator = {
1862
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1863
+ generateContent: mockGenerateContentFn,
1864
+ };
1865
+ client['contentGenerator'] = mockGenerator;
1866
+ // Act
1867
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop-run');
1868
+ for await (const _ of stream) {
1869
+ // consume
1870
+ }
1871
+ // Assert
1872
+ expect(turnStarted).toHaveBeenCalledTimes(1);
1873
+ expect(addAndCheck).toHaveBeenCalled();
1874
+ });
1875
+ });
1876
+ describe('generateContent', () => {
1877
+ it('should call generateContent with the correct parameters', async () => {
1878
+ const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
1879
+ const generationConfig = { temperature: 0.5 };
1880
+ const abortSignal = new AbortController().signal;
1881
+ // Mock countTokens
1882
+ const mockGenerator = {
1883
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
1884
+ generateContent: mockGenerateContentFn,
1885
+ };
1886
+ client['contentGenerator'] = mockGenerator;
1887
+ await client.generateContent(contents, generationConfig, abortSignal);
1888
+ expect(mockGenerateContentFn).toHaveBeenCalledWith({
1889
+ model: 'test-model',
1890
+ config: {
1891
+ abortSignal,
1892
+ systemInstruction: getCoreSystemPrompt(''),
1893
+ temperature: 0.5,
1894
+ topP: 1,
1895
+ },
1896
+ contents,
1897
+ }, 'test-session-id');
1898
+ });
1899
+ it('should use current model from config for content generation', async () => {
1900
+ const initialModel = client['config'].getModel();
1901
+ const contents = [{ role: 'user', parts: [{ text: 'test' }] }];
1902
+ const currentModel = initialModel + '-changed';
1903
+ vi.spyOn(client['config'], 'getModel').mockReturnValueOnce(currentModel);
1904
+ const mockGenerator = {
1905
+ countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
1906
+ generateContent: mockGenerateContentFn,
1907
+ };
1908
+ client['contentGenerator'] = mockGenerator;
1909
+ await client.generateContent(contents, {}, new AbortController().signal);
1910
+ expect(mockGenerateContentFn).not.toHaveBeenCalledWith({
1911
+ model: initialModel,
1912
+ config: expect.any(Object),
1913
+ contents,
1914
+ });
1915
+ expect(mockGenerateContentFn).toHaveBeenCalledWith({
1916
+ model: currentModel,
1917
+ config: expect.any(Object),
1918
+ contents,
1919
+ }, 'test-session-id');
1920
+ });
1921
+ });
1922
+ describe('handleFlashFallback', () => {
1923
+ it('should use current model from config when checking for fallback', async () => {
1924
+ const initialModel = client['config'].getModel();
1925
+ const fallbackModel = DEFAULT_GEMINI_FLASH_MODEL;
1926
+ // mock config been changed
1927
+ const currentModel = initialModel + '-changed';
1928
+ const getModelSpy = vi.spyOn(client['config'], 'getModel');
1929
+ getModelSpy.mockReturnValue(currentModel);
1930
+ const mockFallbackHandler = vi.fn().mockResolvedValue(true);
1931
+ client['config'].flashFallbackHandler = mockFallbackHandler;
1932
+ client['config'].setModel = vi.fn();
1933
+ const result = await client['handleFlashFallback'](AuthType.LOGIN_WITH_GOOGLE);
1934
+ expect(result).toBe(fallbackModel);
1935
+ expect(mockFallbackHandler).toHaveBeenCalledWith(currentModel, fallbackModel, undefined);
1936
+ });
1937
+ });
1938
+ describe('setHistory', () => {
1939
+ it('should strip thought signatures when stripThoughts is true', () => {
1940
+ const mockChat = {
1941
+ setHistory: vi.fn(),
1942
+ };
1943
+ client['chat'] = mockChat;
1944
+ const historyWithThoughts = [
1945
+ {
1946
+ role: 'user',
1947
+ parts: [{ text: 'hello' }],
1948
+ },
1949
+ {
1950
+ role: 'model',
1951
+ parts: [
1952
+ { text: 'thinking...', thoughtSignature: 'thought-123' },
1953
+ {
1954
+ functionCall: { name: 'test', args: {} },
1955
+ thoughtSignature: 'thought-456',
1956
+ },
1957
+ ],
1958
+ },
1959
+ ];
1960
+ client.setHistory(historyWithThoughts, { stripThoughts: true });
1961
+ const expectedHistory = [
1962
+ {
1963
+ role: 'user',
1964
+ parts: [{ text: 'hello' }],
1965
+ },
1966
+ {
1967
+ role: 'model',
1968
+ parts: [
1969
+ { text: 'thinking...' },
1970
+ { functionCall: { name: 'test', args: {} } },
1971
+ ],
1972
+ },
1973
+ ];
1974
+ expect(mockChat.setHistory).toHaveBeenCalledWith(expectedHistory);
1975
+ });
1976
+ it('should not strip thought signatures when stripThoughts is false', () => {
1977
+ const mockChat = {
1978
+ setHistory: vi.fn(),
1979
+ };
1980
+ client['chat'] = mockChat;
1981
+ const historyWithThoughts = [
1982
+ {
1983
+ role: 'user',
1984
+ parts: [{ text: 'hello' }],
1985
+ },
1986
+ {
1987
+ role: 'model',
1988
+ parts: [
1989
+ { text: 'thinking...', thoughtSignature: 'thought-123' },
1990
+ { text: 'ok', thoughtSignature: 'thought-456' },
1991
+ ],
1992
+ },
1993
+ ];
1994
+ client.setHistory(historyWithThoughts, { stripThoughts: false });
1995
+ expect(mockChat.setHistory).toHaveBeenCalledWith(historyWithThoughts);
1996
+ });
1997
+ });
1998
+ describe('initialize', () => {
1999
+ it('should accept extraHistory parameter and pass it to startChat', async () => {
2000
+ const mockStartChat = vi.fn().mockResolvedValue({});
2001
+ client['startChat'] = mockStartChat;
2002
+ const extraHistory = [
2003
+ { role: 'user', parts: [{ text: 'Previous message' }] },
2004
+ { role: 'model', parts: [{ text: 'Previous response' }] },
2005
+ ];
2006
+ const contentGeneratorConfig = {
2007
+ model: 'test-model',
2008
+ apiKey: 'test-key',
2009
+ vertexai: false,
2010
+ authType: AuthType.USE_GEMINI,
2011
+ };
2012
+ await client.initialize(contentGeneratorConfig, extraHistory);
2013
+ expect(mockStartChat).toHaveBeenCalledWith(extraHistory, 'test-model');
2014
+ });
2015
+ it('should use empty array when no extraHistory is provided', async () => {
2016
+ const mockStartChat = vi.fn().mockResolvedValue({});
2017
+ client['startChat'] = mockStartChat;
2018
+ const contentGeneratorConfig = {
2019
+ model: 'test-model',
2020
+ apiKey: 'test-key',
2021
+ vertexai: false,
2022
+ authType: AuthType.USE_GEMINI,
2023
+ };
2024
+ await client.initialize(contentGeneratorConfig);
2025
+ expect(mockStartChat).toHaveBeenCalledWith([], 'test-model');
2026
+ });
2027
+ });
2028
+ describe('reinitialize', () => {
2029
+ it('should reinitialize with preserved user history', async () => {
2030
+ // Mock the initialize method
2031
+ const mockInitialize = vi.fn().mockResolvedValue(undefined);
2032
+ client['initialize'] = mockInitialize;
2033
+ // Set up initial history with environment context + user messages
2034
+ const mockHistory = [
2035
+ { role: 'user', parts: [{ text: 'Environment context' }] },
2036
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
2037
+ { role: 'user', parts: [{ text: 'User message 1' }] },
2038
+ { role: 'model', parts: [{ text: 'Model response 1' }] },
2039
+ ];
2040
+ const mockChat = {
2041
+ getHistory: vi.fn().mockReturnValue(mockHistory),
2042
+ };
2043
+ client['chat'] = mockChat;
2044
+ client['getHistory'] = vi.fn().mockReturnValue(mockHistory);
2045
+ await client.reinitialize();
2046
+ // Should call initialize with preserved user history (excluding first 2 env messages)
2047
+ expect(mockInitialize).toHaveBeenCalledWith(expect.any(Object), // contentGeneratorConfig
2048
+ [
2049
+ { role: 'user', parts: [{ text: 'User message 1' }] },
2050
+ { role: 'model', parts: [{ text: 'Model response 1' }] },
2051
+ ]);
2052
+ });
2053
+ it('should not throw error when chat is not initialized', async () => {
2054
+ client['chat'] = undefined;
2055
+ await expect(client.reinitialize()).resolves.not.toThrow();
2056
+ });
2057
+ });
2058
+ });
2059
+ //# sourceMappingURL=client.test.js.map