@google/gemini-cli-core 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/config/config.d.ts +3 -6
- package/dist/src/config/config.js +5 -7
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +1 -1
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/core/client.d.ts +1 -11
- package/dist/src/core/client.js +17 -165
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +70 -405
- package/dist/src/core/client.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/services/chatCompressionService.d.ts +32 -0
- package/dist/src/services/chatCompressionService.js +163 -0
- package/dist/src/services/chatCompressionService.js.map +1 -0
- package/dist/src/services/chatCompressionService.test.d.ts +6 -0
- package/dist/src/services/chatCompressionService.test.js +211 -0
- package/dist/src/services/chatCompressionService.test.js.map +1 -0
- package/dist/src/services/loopDetectionService.js +4 -1
- package/dist/src/services/loopDetectionService.js.map +1 -1
- package/dist/src/services/loopDetectionService.test.js +10 -0
- package/dist/src/services/loopDetectionService.test.js.map +1 -1
- package/dist/src/utils/environmentContext.d.ts +2 -1
- package/dist/src/utils/environmentContext.js +18 -0
- package/dist/src/utils/environmentContext.js.map +1 -1
- package/dist/src/utils/googleQuotaErrors.js +2 -2
- package/dist/src/utils/googleQuotaErrors.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/google-gemini-cli-core-0.11.0.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 {
|
|
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,
|
|
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
|
-
|
|
335
|
-
|
|
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(
|
|
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
|
|
389
|
-
|
|
390
|
-
const { client, estimatedNewTokenCount: inflatedTokenCount } = setup({
|
|
309
|
+
// Call 1 (Fails): Setup with inflated tokens
|
|
310
|
+
setup({
|
|
391
311
|
originalTokenCount: 100,
|
|
392
|
-
|
|
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
|
|
397
|
-
const shortSummary = 'short';
|
|
316
|
+
// Call 2 (Forced): Re-setup with compressed tokens
|
|
398
317
|
const { estimatedNewTokenCount: compressedTokenCount } = setup({
|
|
399
318
|
originalTokenCount: 100,
|
|
400
|
-
|
|
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
|
-
|
|
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
|
|
428
|
-
const { client, mockOriginalChat, estimatedNewTokenCount } = setup({
|
|
345
|
+
const { client, mockOriginalChat } = setup({
|
|
429
346
|
originalTokenCount: 100,
|
|
430
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
expect(
|
|
449
|
-
expect(
|
|
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(
|
|
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', () => {
|
|
@@ -1555,7 +1216,11 @@ ${JSON.stringify({
|
|
|
1555
1216
|
vi.mocked(ideContextStore.get).mockReturnValue({
|
|
1556
1217
|
workspaceState: {
|
|
1557
1218
|
openFiles: [
|
|
1558
|
-
{
|
|
1219
|
+
{
|
|
1220
|
+
...currentActiveFile,
|
|
1221
|
+
isActive: true,
|
|
1222
|
+
timestamp: Date.now(),
|
|
1223
|
+
},
|
|
1559
1224
|
],
|
|
1560
1225
|
},
|
|
1561
1226
|
});
|