@google/gemini-cli-core 0.12.0-nightly.20251023.c4c0c0d1 → 0.12.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +7 -5
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/agents/subagent-tool-wrapper.test.js +2 -4
  6. package/dist/src/agents/subagent-tool-wrapper.test.js.map +1 -1
  7. package/dist/src/code_assist/oauth2.d.ts +2 -2
  8. package/dist/src/code_assist/oauth2.js +53 -41
  9. package/dist/src/code_assist/oauth2.js.map +1 -1
  10. package/dist/src/code_assist/oauth2.test.js +65 -33
  11. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  12. package/dist/src/code_assist/server.d.ts +3 -3
  13. package/dist/src/code_assist/server.js.map +1 -1
  14. package/dist/src/code_assist/setup.d.ts +2 -2
  15. package/dist/src/code_assist/setup.js.map +1 -1
  16. package/dist/src/config/config.d.ts +12 -3
  17. package/dist/src/config/config.js +35 -9
  18. package/dist/src/config/config.js.map +1 -1
  19. package/dist/src/config/config.test.js +58 -21
  20. package/dist/src/config/config.test.js.map +1 -1
  21. package/dist/src/config/storage.d.ts +1 -0
  22. package/dist/src/config/storage.js +3 -0
  23. package/dist/src/config/storage.js.map +1 -1
  24. package/dist/src/core/client.d.ts +1 -11
  25. package/dist/src/core/client.js +17 -165
  26. package/dist/src/core/client.js.map +1 -1
  27. package/dist/src/core/client.test.js +70 -405
  28. package/dist/src/core/client.test.js.map +1 -1
  29. package/dist/src/core/contentGenerator.js +39 -28
  30. package/dist/src/core/contentGenerator.js.map +1 -1
  31. package/dist/src/core/contentGenerator.test.js +28 -0
  32. package/dist/src/core/contentGenerator.test.js.map +1 -1
  33. package/dist/src/core/coreToolScheduler.d.ts +7 -7
  34. package/dist/src/core/coreToolScheduler.js +312 -183
  35. package/dist/src/core/coreToolScheduler.js.map +1 -1
  36. package/dist/src/core/coreToolScheduler.test.js +239 -9
  37. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  38. package/dist/src/core/fakeContentGenerator.d.ts +33 -0
  39. package/dist/src/core/fakeContentGenerator.js +58 -0
  40. package/dist/src/core/fakeContentGenerator.js.map +1 -0
  41. package/dist/src/core/fakeContentGenerator.test.d.ts +6 -0
  42. package/dist/src/core/fakeContentGenerator.test.js +127 -0
  43. package/dist/src/core/fakeContentGenerator.test.js.map +1 -0
  44. package/dist/src/core/loggingContentGenerator.d.ts +1 -0
  45. package/dist/src/core/loggingContentGenerator.js +113 -33
  46. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  47. package/dist/src/core/nonInteractiveToolExecutor.js +5 -4
  48. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  49. package/dist/src/core/prompts.test.js +30 -108
  50. package/dist/src/core/prompts.test.js.map +1 -1
  51. package/dist/src/core/recordingContentGenerator.d.ts +18 -0
  52. package/dist/src/core/recordingContentGenerator.js +77 -0
  53. package/dist/src/core/recordingContentGenerator.js.map +1 -0
  54. package/dist/src/core/recordingContentGenerator.test.d.ts +6 -0
  55. package/dist/src/core/recordingContentGenerator.test.js +101 -0
  56. package/dist/src/core/recordingContentGenerator.test.js.map +1 -0
  57. package/dist/src/fallback/handler.js +2 -0
  58. package/dist/src/fallback/handler.js.map +1 -1
  59. package/dist/src/generated/git-commit.d.ts +2 -2
  60. package/dist/src/generated/git-commit.js +2 -2
  61. package/dist/src/generated/git-commit.js.map +1 -1
  62. package/dist/src/index.d.ts +3 -0
  63. package/dist/src/index.js +3 -0
  64. package/dist/src/index.js.map +1 -1
  65. package/dist/src/mcp/google-auth-provider.d.ts +2 -0
  66. package/dist/src/mcp/google-auth-provider.js +19 -2
  67. package/dist/src/mcp/google-auth-provider.js.map +1 -1
  68. package/dist/src/mcp/google-auth-provider.test.js +42 -9
  69. package/dist/src/mcp/google-auth-provider.test.js.map +1 -1
  70. package/dist/src/mcp/oauth-provider.js +2 -2
  71. package/dist/src/mcp/oauth-provider.js.map +1 -1
  72. package/dist/src/mcp/oauth-provider.test.js +130 -0
  73. package/dist/src/mcp/oauth-provider.test.js.map +1 -1
  74. package/dist/src/mcp/oauth-token-storage.js +5 -4
  75. package/dist/src/mcp/oauth-token-storage.js.map +1 -1
  76. package/dist/src/mcp/oauth-token-storage.test.js +17 -11
  77. package/dist/src/mcp/oauth-token-storage.test.js.map +1 -1
  78. package/dist/src/mcp/oauth-utils.d.ts +7 -0
  79. package/dist/src/mcp/oauth-utils.js +19 -0
  80. package/dist/src/mcp/oauth-utils.js.map +1 -1
  81. package/dist/src/mcp/oauth-utils.test.js +32 -0
  82. package/dist/src/mcp/oauth-utils.test.js.map +1 -1
  83. package/dist/src/mcp/sa-impersonation-provider.d.ts +0 -6
  84. package/dist/src/mcp/sa-impersonation-provider.js +6 -23
  85. package/dist/src/mcp/sa-impersonation-provider.js.map +1 -1
  86. package/dist/src/mcp/token-storage/base-token-storage.test.js +75 -84
  87. package/dist/src/mcp/token-storage/base-token-storage.test.js.map +1 -1
  88. package/dist/src/mcp/token-storage/keychain-token-storage.d.ts +6 -2
  89. package/dist/src/mcp/token-storage/keychain-token-storage.js +62 -6
  90. package/dist/src/mcp/token-storage/keychain-token-storage.js.map +1 -1
  91. package/dist/src/mcp/token-storage/keychain-token-storage.test.js +54 -3
  92. package/dist/src/mcp/token-storage/keychain-token-storage.test.js.map +1 -1
  93. package/dist/src/mcp/token-storage/types.d.ts +6 -0
  94. package/dist/src/mcp/token-storage/types.js.map +1 -1
  95. package/dist/src/policy/policy-engine.js +4 -0
  96. package/dist/src/policy/policy-engine.js.map +1 -1
  97. package/dist/src/services/chatCompressionService.d.ts +32 -0
  98. package/dist/src/services/chatCompressionService.js +164 -0
  99. package/dist/src/services/chatCompressionService.js.map +1 -0
  100. package/dist/src/services/chatCompressionService.test.d.ts +6 -0
  101. package/dist/src/services/chatCompressionService.test.js +211 -0
  102. package/dist/src/services/chatCompressionService.test.js.map +1 -0
  103. package/dist/src/services/fileDiscoveryService.d.ts +0 -8
  104. package/dist/src/services/fileDiscoveryService.js +5 -33
  105. package/dist/src/services/fileDiscoveryService.js.map +1 -1
  106. package/dist/src/services/fileDiscoveryService.test.js +11 -11
  107. package/dist/src/services/fileDiscoveryService.test.js.map +1 -1
  108. package/dist/src/services/loopDetectionService.js +19 -9
  109. package/dist/src/services/loopDetectionService.js.map +1 -1
  110. package/dist/src/services/loopDetectionService.test.js +40 -11
  111. package/dist/src/services/loopDetectionService.test.js.map +1 -1
  112. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +1 -0
  113. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +40 -33
  114. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  115. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +1 -1
  116. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  117. package/dist/src/telemetry/index.d.ts +1 -0
  118. package/dist/src/telemetry/index.js +1 -0
  119. package/dist/src/telemetry/index.js.map +1 -1
  120. package/dist/src/telemetry/loggers.js +10 -17
  121. package/dist/src/telemetry/loggers.js.map +1 -1
  122. package/dist/src/telemetry/loggers.test.js +173 -30
  123. package/dist/src/telemetry/loggers.test.js.map +1 -1
  124. package/dist/src/telemetry/metrics.d.ts +1 -1
  125. package/dist/src/telemetry/metrics.js +1 -1
  126. package/dist/src/telemetry/metrics.js.map +1 -1
  127. package/dist/src/telemetry/metrics.test.js +2 -2
  128. package/dist/src/telemetry/metrics.test.js.map +1 -1
  129. package/dist/src/telemetry/semantic.d.ts +82 -0
  130. package/dist/src/telemetry/semantic.js +269 -0
  131. package/dist/src/telemetry/semantic.js.map +1 -0
  132. package/dist/src/telemetry/semantic.test.d.ts +6 -0
  133. package/dist/src/telemetry/semantic.test.js +387 -0
  134. package/dist/src/telemetry/semantic.test.js.map +1 -0
  135. package/dist/src/telemetry/trace.d.ts +46 -0
  136. package/dist/src/telemetry/trace.js +121 -0
  137. package/dist/src/telemetry/trace.js.map +1 -0
  138. package/dist/src/telemetry/types.d.ts +37 -18
  139. package/dist/src/telemetry/types.js +107 -36
  140. package/dist/src/telemetry/types.js.map +1 -1
  141. package/dist/src/telemetry/uiTelemetry.js +6 -6
  142. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  143. package/dist/src/telemetry/uiTelemetry.test.js +88 -66
  144. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  145. package/dist/src/tools/edit.d.ts +3 -2
  146. package/dist/src/tools/edit.js +15 -11
  147. package/dist/src/tools/edit.js.map +1 -1
  148. package/dist/src/tools/edit.test.js +19 -0
  149. package/dist/src/tools/edit.test.js.map +1 -1
  150. package/dist/src/tools/glob.js +4 -2
  151. package/dist/src/tools/glob.js.map +1 -1
  152. package/dist/src/tools/glob.test.js +203 -199
  153. package/dist/src/tools/glob.test.js.map +1 -1
  154. package/dist/src/tools/grep.js +2 -2
  155. package/dist/src/tools/grep.js.map +1 -1
  156. package/dist/src/tools/ls.js +2 -1
  157. package/dist/src/tools/ls.js.map +1 -1
  158. package/dist/src/tools/ls.test.js +0 -7
  159. package/dist/src/tools/ls.test.js.map +1 -1
  160. package/dist/src/tools/mcp-client-manager.js +2 -1
  161. package/dist/src/tools/mcp-client-manager.js.map +1 -1
  162. package/dist/src/tools/mcp-client.d.ts +4 -1
  163. package/dist/src/tools/mcp-client.js +71 -92
  164. package/dist/src/tools/mcp-client.js.map +1 -1
  165. package/dist/src/tools/mcp-client.test.js +16 -6
  166. package/dist/src/tools/mcp-client.test.js.map +1 -1
  167. package/dist/src/tools/mcp-tool.d.ts +3 -2
  168. package/dist/src/tools/mcp-tool.js +12 -10
  169. package/dist/src/tools/mcp-tool.js.map +1 -1
  170. package/dist/src/tools/memoryTool.d.ts +5 -3
  171. package/dist/src/tools/memoryTool.js +9 -7
  172. package/dist/src/tools/memoryTool.js.map +1 -1
  173. package/dist/src/tools/read-file.js +7 -3
  174. package/dist/src/tools/read-file.js.map +1 -1
  175. package/dist/src/tools/read-file.test.js +25 -2
  176. package/dist/src/tools/read-file.test.js.map +1 -1
  177. package/dist/src/tools/shell.d.ts +6 -4
  178. package/dist/src/tools/shell.js +18 -12
  179. package/dist/src/tools/shell.js.map +1 -1
  180. package/dist/src/tools/shell.test.js +7 -0
  181. package/dist/src/tools/shell.test.js.map +1 -1
  182. package/dist/src/tools/smart-edit.d.ts +2 -1
  183. package/dist/src/tools/smart-edit.js +10 -9
  184. package/dist/src/tools/smart-edit.js.map +1 -1
  185. package/dist/src/tools/tool-registry.d.ts +5 -1
  186. package/dist/src/tools/tool-registry.js +10 -3
  187. package/dist/src/tools/tool-registry.js.map +1 -1
  188. package/dist/src/tools/tools.d.ts +6 -0
  189. package/dist/src/tools/tools.js +29 -17
  190. package/dist/src/tools/tools.js.map +1 -1
  191. package/dist/src/tools/web-fetch.js +1 -12
  192. package/dist/src/tools/web-fetch.js.map +1 -1
  193. package/dist/src/tools/web-fetch.test.js +2 -2
  194. package/dist/src/tools/web-fetch.test.js.map +1 -1
  195. package/dist/src/tools/write-file.d.ts +2 -1
  196. package/dist/src/tools/write-file.js +6 -6
  197. package/dist/src/tools/write-file.js.map +1 -1
  198. package/dist/src/tools/write-todos.d.ts +2 -1
  199. package/dist/src/tools/write-todos.js +5 -2
  200. package/dist/src/tools/write-todos.js.map +1 -1
  201. package/dist/src/utils/environmentContext.d.ts +2 -1
  202. package/dist/src/utils/environmentContext.js +18 -0
  203. package/dist/src/utils/environmentContext.js.map +1 -1
  204. package/dist/src/utils/errorParsing.d.ts +1 -1
  205. package/dist/src/utils/errorParsing.js +5 -33
  206. package/dist/src/utils/errorParsing.js.map +1 -1
  207. package/dist/src/utils/errorParsing.test.js +0 -88
  208. package/dist/src/utils/errorParsing.test.js.map +1 -1
  209. package/dist/src/utils/errors.d.ts +3 -0
  210. package/dist/src/utils/errors.js +6 -0
  211. package/dist/src/utils/errors.js.map +1 -1
  212. package/dist/src/utils/events.d.ts +19 -1
  213. package/dist/src/utils/events.js +9 -0
  214. package/dist/src/utils/events.js.map +1 -1
  215. package/dist/src/utils/extensionLoader.d.ts +38 -0
  216. package/dist/src/utils/extensionLoader.js +20 -0
  217. package/dist/src/utils/extensionLoader.js.map +1 -0
  218. package/dist/src/utils/flashFallback.test.js +26 -45
  219. package/dist/src/utils/flashFallback.test.js.map +1 -1
  220. package/dist/src/utils/getFolderStructure.js +7 -16
  221. package/dist/src/utils/getFolderStructure.js.map +1 -1
  222. package/dist/src/utils/googleErrors.d.ts +104 -0
  223. package/dist/src/utils/googleErrors.js +152 -0
  224. package/dist/src/utils/googleErrors.js.map +1 -0
  225. package/dist/src/utils/googleErrors.test.d.ts +6 -0
  226. package/dist/src/utils/googleErrors.test.js +301 -0
  227. package/dist/src/utils/googleErrors.test.js.map +1 -0
  228. package/dist/src/utils/googleQuotaErrors.d.ts +35 -0
  229. package/dist/src/utils/googleQuotaErrors.js +131 -0
  230. package/dist/src/utils/googleQuotaErrors.js.map +1 -0
  231. package/dist/src/utils/googleQuotaErrors.test.d.ts +6 -0
  232. package/dist/src/utils/googleQuotaErrors.test.js +281 -0
  233. package/dist/src/utils/googleQuotaErrors.test.js.map +1 -0
  234. package/dist/src/utils/ignorePatterns.test.js +26 -30
  235. package/dist/src/utils/ignorePatterns.test.js.map +1 -1
  236. package/dist/src/utils/memoryDiscovery.d.ts +2 -2
  237. package/dist/src/utils/memoryDiscovery.js +3 -2
  238. package/dist/src/utils/memoryDiscovery.js.map +1 -1
  239. package/dist/src/utils/memoryDiscovery.test.js +19 -35
  240. package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
  241. package/dist/src/utils/paths.js +126 -26
  242. package/dist/src/utils/paths.js.map +1 -1
  243. package/dist/src/utils/paths.test.js +200 -68
  244. package/dist/src/utils/paths.test.js.map +1 -1
  245. package/dist/src/utils/quotaErrorDetection.d.ts +0 -2
  246. package/dist/src/utils/quotaErrorDetection.js +0 -46
  247. package/dist/src/utils/quotaErrorDetection.js.map +1 -1
  248. package/dist/src/utils/retry.js +41 -145
  249. package/dist/src/utils/retry.js.map +1 -1
  250. package/dist/src/utils/retry.test.js +31 -110
  251. package/dist/src/utils/retry.test.js.map +1 -1
  252. package/dist/src/utils/shell-utils.test.js +70 -0
  253. package/dist/src/utils/shell-utils.test.js.map +1 -1
  254. package/dist/src/utils/workspaceContext.js +1 -1
  255. package/dist/src/utils/workspaceContext.js.map +1 -1
  256. package/dist/tsconfig.tsbuildinfo +1 -1
  257. package/package.json +2 -2
  258. package/dist/google-gemini-cli-core-0.12.0-nightly.20251022.0542de95.tgz +0 -0
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
7
- import { findCompressSplitPoint, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
7
+ import { isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
8
8
  import { AuthType, } from './contentGenerator.js';
9
9
  import {} from './geminiChat.js';
10
10
  import { CompressionStatus, GeminiEventType, Turn, } from './turn.js';
@@ -14,8 +14,9 @@ import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
14
14
  import { setSimulate429 } from '../utils/testUtils.js';
15
15
  import { tokenLimit } from './tokenLimits.js';
16
16
  import { ideContextStore } from '../ide/ideContext.js';
17
- import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
18
17
  import { uiTelemetryService } from '../telemetry/uiTelemetry.js';
18
+ import { ChatCompressionService } from '../services/chatCompressionService.js';
19
+ vi.mock('../services/chatCompressionService.js');
19
20
  // Mock fs module to prevent actual file system operations during tests
20
21
  const mockFileSystem = new Map();
21
22
  vi.mock('node:fs', () => {
@@ -95,70 +96,6 @@ async function fromAsync(promise) {
95
96
  }
96
97
  return results;
97
98
  }
98
- describe('findCompressSplitPoint', () => {
99
- it('should throw an error for non-positive numbers', () => {
100
- expect(() => findCompressSplitPoint([], 0)).toThrow('Fraction must be between 0 and 1');
101
- });
102
- it('should throw an error for a fraction greater than or equal to 1', () => {
103
- expect(() => findCompressSplitPoint([], 1)).toThrow('Fraction must be between 0 and 1');
104
- });
105
- it('should handle an empty history', () => {
106
- expect(findCompressSplitPoint([], 0.5)).toBe(0);
107
- });
108
- it('should handle a fraction in the middle', () => {
109
- const history = [
110
- { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (19%)
111
- { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (40%)
112
- { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (60%)
113
- { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (80%)
114
- { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65 (100%)
115
- ];
116
- expect(findCompressSplitPoint(history, 0.5)).toBe(4);
117
- });
118
- it('should handle a fraction of last index', () => {
119
- const history = [
120
- { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (19%)
121
- { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (40%)
122
- { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (60%)
123
- { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (80%)
124
- { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65 (100%)
125
- ];
126
- expect(findCompressSplitPoint(history, 0.9)).toBe(4);
127
- });
128
- it('should handle a fraction of after last index', () => {
129
- const history = [
130
- { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (24%%)
131
- { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (50%)
132
- { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (74%)
133
- { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (100%)
134
- ];
135
- expect(findCompressSplitPoint(history, 0.8)).toBe(4);
136
- });
137
- it('should return earlier splitpoint if no valid ones are after threshhold', () => {
138
- const history = [
139
- { role: 'user', parts: [{ text: 'This is the first message.' }] },
140
- { role: 'model', parts: [{ text: 'This is the second message.' }] },
141
- { role: 'user', parts: [{ text: 'This is the third message.' }] },
142
- { role: 'model', parts: [{ functionCall: {} }] },
143
- ];
144
- // Can't return 4 because the previous item has a function call.
145
- expect(findCompressSplitPoint(history, 0.99)).toBe(2);
146
- });
147
- it('should handle a history with only one item', () => {
148
- const historyWithEmptyParts = [
149
- { role: 'user', parts: [{ text: 'Message 1' }] },
150
- ];
151
- expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(0);
152
- });
153
- it('should handle history with weird parts', () => {
154
- const historyWithEmptyParts = [
155
- { role: 'user', parts: [{ text: 'Message 1' }] },
156
- { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
157
- { role: 'user', parts: [{ text: 'Message 2' }] },
158
- ];
159
- expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(2);
160
- });
161
- });
162
99
  describe('isThinkingSupported', () => {
163
100
  it('should return true for gemini-2.5', () => {
164
101
  expect(isThinkingSupported('gemini-2.5')).toBe(true);
@@ -194,6 +131,14 @@ describe('Gemini Client (client.ts)', () => {
194
131
  beforeEach(async () => {
195
132
  vi.resetAllMocks();
196
133
  vi.mocked(uiTelemetryService.setLastPromptTokenCount).mockClear();
134
+ vi.mocked(ChatCompressionService.prototype.compress).mockResolvedValue({
135
+ newHistory: null,
136
+ info: {
137
+ originalTokenCount: 0,
138
+ newTokenCount: 0,
139
+ compressionStatus: CompressionStatus.NOOP,
140
+ },
141
+ });
197
142
  mockGenerateContentFn = vi.fn().mockResolvedValue({
198
143
  candidates: [{ content: { parts: [{ text: '{"key": "value"}' }] } }],
199
144
  });
@@ -324,82 +269,56 @@ describe('Gemini Client (client.ts)', () => {
324
269
  function setup({ chatHistory = [
325
270
  { role: 'user', parts: [{ text: 'Long conversation' }] },
326
271
  { role: 'model', parts: [{ text: 'Long response' }] },
327
- ], originalTokenCount = 1000, summaryText = 'This is a summary.', } = {}) {
272
+ ], originalTokenCount = 1000, newTokenCount = 500, compressionStatus = CompressionStatus.COMPRESSED, } = {}) {
328
273
  const mockOriginalChat = {
329
274
  getHistory: vi.fn((_curated) => chatHistory),
330
275
  setHistory: vi.fn(),
331
276
  };
332
277
  client['chat'] = mockOriginalChat;
333
278
  vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
334
- mockGenerateContentFn.mockResolvedValue({
335
- candidates: [
336
- {
337
- content: {
338
- role: 'model',
339
- parts: [{ text: summaryText }],
340
- },
341
- },
342
- ],
343
- });
344
- // Calculate what the new history will be
345
- const splitPoint = findCompressSplitPoint(chatHistory, 0.7); // 1 - 0.3
346
- const historyToKeep = chatHistory.slice(splitPoint);
347
- // This is the history that the new chat will have.
348
- // It includes the default startChat history + the extra history from tryCompressChat
349
- const newCompressedHistory = [
350
- // Mocked envParts + canned response from startChat
351
- {
352
- role: 'user',
353
- parts: [{ text: 'Mocked env context' }],
354
- },
355
- {
356
- role: 'model',
357
- parts: [{ text: 'Got it. Thanks for the context!' }],
358
- },
359
- // extraHistory from tryCompressChat
360
- {
361
- role: 'user',
362
- parts: [{ text: summaryText }],
363
- },
364
- {
365
- role: 'model',
366
- parts: [{ text: 'Got it. Thanks for the additional context!' }],
367
- },
368
- ...historyToKeep,
279
+ const newHistory = [
280
+ { role: 'user', parts: [{ text: 'Summary' }] },
281
+ { role: 'model', parts: [{ text: 'Got it' }] },
369
282
  ];
283
+ vi.mocked(ChatCompressionService.prototype.compress).mockResolvedValue({
284
+ newHistory: compressionStatus === CompressionStatus.COMPRESSED
285
+ ? newHistory
286
+ : null,
287
+ info: {
288
+ originalTokenCount,
289
+ newTokenCount,
290
+ compressionStatus,
291
+ },
292
+ });
370
293
  const mockNewChat = {
371
- getHistory: vi.fn().mockReturnValue(newCompressedHistory),
294
+ getHistory: vi.fn().mockReturnValue(newHistory),
372
295
  setHistory: vi.fn(),
373
296
  };
374
297
  client['startChat'] = vi
375
298
  .fn()
376
299
  .mockResolvedValue(mockNewChat);
377
- const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
378
- const estimatedNewTokenCount = Math.floor(totalChars / 4);
379
300
  return {
380
301
  client,
381
302
  mockOriginalChat,
382
303
  mockNewChat,
383
- estimatedNewTokenCount,
304
+ estimatedNewTokenCount: newTokenCount,
384
305
  };
385
306
  }
386
307
  describe('when compression inflates the token count', () => {
387
308
  it('allows compression to be forced/manual after a failure', async () => {
388
- // Call 1 (Fails): Setup with a long summary to inflate tokens
389
- const longSummary = 'long summary '.repeat(100);
390
- const { client, estimatedNewTokenCount: inflatedTokenCount } = setup({
309
+ // Call 1 (Fails): Setup with inflated tokens
310
+ setup({
391
311
  originalTokenCount: 100,
392
- summaryText: longSummary,
312
+ newTokenCount: 200,
313
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
393
314
  });
394
- expect(inflatedTokenCount).toBeGreaterThan(100); // Ensure setup is correct
395
315
  await client.tryCompressChat('prompt-id-4', false); // Fails
396
- // Call 2 (Forced): Re-setup with a short summary
397
- const shortSummary = 'short';
316
+ // Call 2 (Forced): Re-setup with compressed tokens
398
317
  const { estimatedNewTokenCount: compressedTokenCount } = setup({
399
318
  originalTokenCount: 100,
400
- summaryText: shortSummary,
319
+ newTokenCount: 50,
320
+ compressionStatus: CompressionStatus.COMPRESSED,
401
321
  });
402
- expect(compressedTokenCount).toBeLessThanOrEqual(100); // Ensure setup is correct
403
322
  const result = await client.tryCompressChat('prompt-id-4', true); // Forced
404
323
  expect(result).toEqual({
405
324
  compressionStatus: CompressionStatus.COMPRESSED,
@@ -408,12 +327,11 @@ describe('Gemini Client (client.ts)', () => {
408
327
  });
409
328
  });
410
329
  it('yields the result even if the compression inflated the tokens', async () => {
411
- const longSummary = 'long summary '.repeat(100);
412
330
  const { client, estimatedNewTokenCount } = setup({
413
331
  originalTokenCount: 100,
414
- summaryText: longSummary,
332
+ newTokenCount: 200,
333
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
415
334
  });
416
- expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
417
335
  const result = await client.tryCompressChat('prompt-id-4', false);
418
336
  expect(result).toEqual({
419
337
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
@@ -424,47 +342,52 @@ describe('Gemini Client (client.ts)', () => {
424
342
  expect(uiTelemetryService.setLastPromptTokenCount).not.toHaveBeenCalled();
425
343
  });
426
344
  it('does not manipulate the source chat', async () => {
427
- const longSummary = 'long summary '.repeat(100);
428
- const { client, mockOriginalChat, estimatedNewTokenCount } = setup({
345
+ const { client, mockOriginalChat } = setup({
429
346
  originalTokenCount: 100,
430
- summaryText: longSummary,
347
+ newTokenCount: 200,
348
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
431
349
  });
432
- expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
433
350
  await client.tryCompressChat('prompt-id-4', false);
434
351
  // On failure, the chat should NOT be replaced
435
352
  expect(client['chat']).toBe(mockOriginalChat);
436
353
  });
437
- it('will not attempt to compress context after a failure', async () => {
438
- const longSummary = 'long summary '.repeat(100);
439
- const { client, estimatedNewTokenCount } = setup({
354
+ it.skip('will not attempt to compress context after a failure', async () => {
355
+ const { client } = setup({
440
356
  originalTokenCount: 100,
441
- summaryText: longSummary,
357
+ newTokenCount: 200,
358
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
442
359
  });
443
- expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
444
360
  await client.tryCompressChat('prompt-id-4', false); // This fails and sets hasFailedCompressionAttempt = true
361
+ // Mock the next call to return NOOP
362
+ vi.mocked(ChatCompressionService.prototype.compress).mockResolvedValueOnce({
363
+ newHistory: null,
364
+ info: {
365
+ originalTokenCount: 0,
366
+ newTokenCount: 0,
367
+ compressionStatus: CompressionStatus.NOOP,
368
+ },
369
+ });
445
370
  // This call should now be a NOOP
446
371
  const result = await client.tryCompressChat('prompt-id-5', false);
447
- // generateContent (for summary) should only have been called once
448
- expect(mockGenerateContentFn).toHaveBeenCalledTimes(1);
449
- expect(result).toEqual({
450
- compressionStatus: CompressionStatus.NOOP,
451
- newTokenCount: 0,
452
- originalTokenCount: 0,
453
- });
372
+ expect(result.compressionStatus).toBe(CompressionStatus.NOOP);
373
+ expect(ChatCompressionService.prototype.compress).toHaveBeenCalledTimes(2);
374
+ expect(ChatCompressionService.prototype.compress).toHaveBeenLastCalledWith(expect.anything(), 'prompt-id-5', false, expect.anything(), expect.anything(), true);
454
375
  });
455
376
  });
456
377
  it('should not trigger summarization if token count is below threshold', async () => {
457
378
  const MOCKED_TOKEN_LIMIT = 1000;
458
- vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
459
- mockGetHistory.mockReturnValue([
460
- { role: 'user', parts: [{ text: '...history...' }] },
461
- ]);
462
379
  const originalTokenCount = MOCKED_TOKEN_LIMIT * 0.699;
463
- vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
380
+ vi.mocked(ChatCompressionService.prototype.compress).mockResolvedValue({
381
+ newHistory: null,
382
+ info: {
383
+ originalTokenCount,
384
+ newTokenCount: originalTokenCount,
385
+ compressionStatus: CompressionStatus.NOOP,
386
+ },
387
+ });
464
388
  const initialChat = client.getChat();
465
389
  const result = await client.tryCompressChat('prompt-id-2', false);
466
390
  const newChat = client.getChat();
467
- expect(tokenLimit).toHaveBeenCalled();
468
391
  expect(result).toEqual({
469
392
  compressionStatus: CompressionStatus.NOOP,
470
393
  newTokenCount: originalTokenCount,
@@ -476,6 +399,8 @@ describe('Gemini Client (client.ts)', () => {
476
399
  const { client } = setup({
477
400
  chatHistory: [{ role: 'user', parts: [{ text: 'hi' }] }],
478
401
  originalTokenCount: 50,
402
+ newTokenCount: 50,
403
+ compressionStatus: CompressionStatus.NOOP,
479
404
  });
480
405
  const result = await client.tryCompressChat('prompt-id-noop', false);
481
406
  expect(result).toEqual({
@@ -483,270 +408,6 @@ describe('Gemini Client (client.ts)', () => {
483
408
  originalTokenCount: 50,
484
409
  newTokenCount: 50,
485
410
  });
486
- expect(mockGenerateContentFn).not.toHaveBeenCalled();
487
- });
488
- it('logs a telemetry event when compressing', async () => {
489
- vi.spyOn(ClearcutLogger.prototype, 'logChatCompressionEvent');
490
- const MOCKED_TOKEN_LIMIT = 1000;
491
- const MOCKED_CONTEXT_PERCENTAGE_THRESHOLD = 0.5;
492
- vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
493
- contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
494
- });
495
- const history = [
496
- { role: 'user', parts: [{ text: '...history...' }] },
497
- { role: 'model', parts: [{ text: '...history...' }] },
498
- { role: 'user', parts: [{ text: '...history...' }] },
499
- { role: 'model', parts: [{ text: '...history...' }] },
500
- { role: 'user', parts: [{ text: '...history...' }] },
501
- { role: 'model', parts: [{ text: '...history...' }] },
502
- ];
503
- mockGetHistory.mockReturnValue(history);
504
- const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
505
- vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
506
- // We need to control the estimated new token count.
507
- // We mock startChat to return a chat with a known history.
508
- const summaryText = 'This is a summary.';
509
- const splitPoint = findCompressSplitPoint(history, 0.7);
510
- const historyToKeep = history.slice(splitPoint);
511
- const newCompressedHistory = [
512
- { role: 'user', parts: [{ text: 'Mocked env context' }] },
513
- { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
514
- { role: 'user', parts: [{ text: summaryText }] },
515
- {
516
- role: 'model',
517
- parts: [{ text: 'Got it. Thanks for the additional context!' }],
518
- },
519
- ...historyToKeep,
520
- ];
521
- const mockNewChat = {
522
- getHistory: vi.fn().mockReturnValue(newCompressedHistory),
523
- };
524
- client['startChat'] = vi
525
- .fn()
526
- .mockResolvedValue(mockNewChat);
527
- const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
528
- const newTokenCount = Math.floor(totalChars / 4);
529
- // Mock the summary response from the chat
530
- mockGenerateContentFn.mockResolvedValue({
531
- candidates: [
532
- {
533
- content: {
534
- role: 'model',
535
- parts: [{ text: summaryText }],
536
- },
537
- },
538
- ],
539
- });
540
- await client.tryCompressChat('prompt-id-3', false);
541
- expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
542
- tokens_before: originalTokenCount,
543
- tokens_after: newTokenCount,
544
- }));
545
- expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(newTokenCount);
546
- expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
547
- });
548
- it('should trigger summarization if token count is at threshold with contextPercentageThreshold setting', async () => {
549
- const MOCKED_TOKEN_LIMIT = 1000;
550
- const MOCKED_CONTEXT_PERCENTAGE_THRESHOLD = 0.5;
551
- vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
552
- vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
553
- contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
554
- });
555
- const history = [
556
- { role: 'user', parts: [{ text: '...history...' }] },
557
- { role: 'model', parts: [{ text: '...history...' }] },
558
- { role: 'user', parts: [{ text: '...history...' }] },
559
- { role: 'model', parts: [{ text: '...history...' }] },
560
- { role: 'user', parts: [{ text: '...history...' }] },
561
- { role: 'model', parts: [{ text: '...history...' }] },
562
- ];
563
- mockGetHistory.mockReturnValue(history);
564
- const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
565
- vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
566
- // Mock summary and new chat
567
- const summaryText = 'This is a summary.';
568
- const splitPoint = findCompressSplitPoint(history, 0.7);
569
- const historyToKeep = history.slice(splitPoint);
570
- const newCompressedHistory = [
571
- { role: 'user', parts: [{ text: 'Mocked env context' }] },
572
- { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
573
- { role: 'user', parts: [{ text: summaryText }] },
574
- {
575
- role: 'model',
576
- parts: [{ text: 'Got it. Thanks for the additional context!' }],
577
- },
578
- ...historyToKeep,
579
- ];
580
- const mockNewChat = {
581
- getHistory: vi.fn().mockReturnValue(newCompressedHistory),
582
- };
583
- client['startChat'] = vi
584
- .fn()
585
- .mockResolvedValue(mockNewChat);
586
- const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
587
- const newTokenCount = Math.floor(totalChars / 4);
588
- // Mock the summary response from the chat
589
- mockGenerateContentFn.mockResolvedValue({
590
- candidates: [
591
- {
592
- content: {
593
- role: 'model',
594
- parts: [{ text: summaryText }],
595
- },
596
- },
597
- ],
598
- });
599
- const initialChat = client.getChat();
600
- const result = await client.tryCompressChat('prompt-id-3', false);
601
- const newChat = client.getChat();
602
- expect(tokenLimit).toHaveBeenCalled();
603
- expect(mockGenerateContentFn).toHaveBeenCalled();
604
- // Assert that summarization happened and returned the correct stats
605
- expect(result).toEqual({
606
- compressionStatus: CompressionStatus.COMPRESSED,
607
- originalTokenCount,
608
- newTokenCount,
609
- });
610
- // Assert that the chat was reset
611
- expect(newChat).not.toBe(initialChat);
612
- });
613
- it('should not compress across a function call response', async () => {
614
- const MOCKED_TOKEN_LIMIT = 1000;
615
- vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
616
- const history = [
617
- { role: 'user', parts: [{ text: '...history 1...' }] },
618
- { role: 'model', parts: [{ text: '...history 2...' }] },
619
- { role: 'user', parts: [{ text: '...history 3...' }] },
620
- { role: 'model', parts: [{ text: '...history 4...' }] },
621
- { role: 'user', parts: [{ text: '...history 5...' }] },
622
- { role: 'model', parts: [{ text: '...history 6...' }] },
623
- { role: 'user', parts: [{ text: '...history 7...' }] },
624
- { role: 'model', parts: [{ text: '...history 8...' }] },
625
- // Normally we would break here, but we have a function response.
626
- {
627
- role: 'user',
628
- parts: [{ functionResponse: { name: '...history 8...' } }],
629
- },
630
- { role: 'model', parts: [{ text: '...history 10...' }] },
631
- // Instead we will break here.
632
- { role: 'user', parts: [{ text: '...history 10...' }] },
633
- ];
634
- mockGetHistory.mockReturnValue(history);
635
- const originalTokenCount = 1000 * 0.7;
636
- vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
637
- // Mock summary and new chat
638
- const summaryText = 'This is a summary.';
639
- const splitPoint = findCompressSplitPoint(history, 0.7); // This should be 10
640
- expect(splitPoint).toBe(10); // Verify split point logic
641
- const historyToKeep = history.slice(splitPoint); // Should keep last user message
642
- expect(historyToKeep).toEqual([
643
- { role: 'user', parts: [{ text: '...history 10...' }] },
644
- ]);
645
- const newCompressedHistory = [
646
- { role: 'user', parts: [{ text: 'Mocked env context' }] },
647
- { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
648
- { role: 'user', parts: [{ text: summaryText }] },
649
- {
650
- role: 'model',
651
- parts: [{ text: 'Got it. Thanks for the additional context!' }],
652
- },
653
- ...historyToKeep,
654
- ];
655
- const mockNewChat = {
656
- getHistory: vi.fn().mockReturnValue(newCompressedHistory),
657
- };
658
- client['startChat'] = vi
659
- .fn()
660
- .mockResolvedValue(mockNewChat);
661
- const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
662
- const newTokenCount = Math.floor(totalChars / 4);
663
- // Mock the summary response from the chat
664
- mockGenerateContentFn.mockResolvedValue({
665
- candidates: [
666
- {
667
- content: {
668
- role: 'model',
669
- parts: [{ text: summaryText }],
670
- },
671
- },
672
- ],
673
- });
674
- const initialChat = client.getChat();
675
- const result = await client.tryCompressChat('prompt-id-3', false);
676
- const newChat = client.getChat();
677
- expect(tokenLimit).toHaveBeenCalled();
678
- expect(mockGenerateContentFn).toHaveBeenCalled();
679
- // Assert that summarization happened and returned the correct stats
680
- expect(result).toEqual({
681
- compressionStatus: CompressionStatus.COMPRESSED,
682
- originalTokenCount,
683
- newTokenCount,
684
- });
685
- // Assert that the chat was reset
686
- expect(newChat).not.toBe(initialChat);
687
- // 1. standard start context message (env)
688
- // 2. standard canned model response
689
- // 3. compressed summary message (user)
690
- // 4. standard canned model response
691
- // 5. The last user message (historyToKeep)
692
- expect(newChat.getHistory().length).toEqual(5);
693
- });
694
- it('should always trigger summarization when force is true, regardless of token count', async () => {
695
- const history = [
696
- { role: 'user', parts: [{ text: '...history...' }] },
697
- { role: 'model', parts: [{ text: '...history...' }] },
698
- { role: 'user', parts: [{ text: '...history...' }] },
699
- { role: 'model', parts: [{ text: '...history...' }] },
700
- { role: 'user', parts: [{ text: '...history...' }] },
701
- { role: 'model', parts: [{ text: '...history...' }] },
702
- ];
703
- mockGetHistory.mockReturnValue(history);
704
- const originalTokenCount = 100; // Well below threshold, but > estimated new count
705
- vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
706
- // Mock summary and new chat
707
- const summaryText = 'This is a summary.';
708
- const splitPoint = findCompressSplitPoint(history, 0.7);
709
- const historyToKeep = history.slice(splitPoint);
710
- const newCompressedHistory = [
711
- { role: 'user', parts: [{ text: 'Mocked env context' }] },
712
- { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
713
- { role: 'user', parts: [{ text: summaryText }] },
714
- {
715
- role: 'model',
716
- parts: [{ text: 'Got it. Thanks for the additional context!' }],
717
- },
718
- ...historyToKeep,
719
- ];
720
- const mockNewChat = {
721
- getHistory: vi.fn().mockReturnValue(newCompressedHistory),
722
- };
723
- client['startChat'] = vi
724
- .fn()
725
- .mockResolvedValue(mockNewChat);
726
- const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
727
- const newTokenCount = Math.floor(totalChars / 4);
728
- // Mock the summary response from the chat
729
- mockGenerateContentFn.mockResolvedValue({
730
- candidates: [
731
- {
732
- content: {
733
- role: 'model',
734
- parts: [{ text: summaryText }],
735
- },
736
- },
737
- ],
738
- });
739
- const initialChat = client.getChat();
740
- const result = await client.tryCompressChat('prompt-id-1', true); // force = true
741
- const newChat = client.getChat();
742
- expect(mockGenerateContentFn).toHaveBeenCalled();
743
- expect(result).toEqual({
744
- compressionStatus: CompressionStatus.COMPRESSED,
745
- originalTokenCount,
746
- newTokenCount,
747
- });
748
- // Assert that the chat was reset
749
- expect(newChat).not.toBe(initialChat);
750
411
  });
751
412
  });
752
413
  describe('sendMessageStream', () => {
@@ -1554,7 +1215,11 @@ ${JSON.stringify({
1554
1215
  vi.mocked(ideContextStore.get).mockReturnValue({
1555
1216
  workspaceState: {
1556
1217
  openFiles: [
1557
- { ...currentActiveFile, isActive: true, timestamp: Date.now() },
1218
+ {
1219
+ ...currentActiveFile,
1220
+ isActive: true,
1221
+ timestamp: Date.now(),
1222
+ },
1558
1223
  ],
1559
1224
  },
1560
1225
  });