@google/gemini-cli-core 0.21.0-nightly.20251205.f4f2bcbd9 → 0.21.0-nightly.20251206.3cf44acc0

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 (52) hide show
  1. package/dist/google-gemini-cli-core-0.21.0-nightly.20251204.3da4fd5f7.tgz +0 -0
  2. package/dist/src/core/geminiChat.js +16 -5
  3. package/dist/src/core/geminiChat.js.map +1 -1
  4. package/dist/src/core/geminiChat.test.js +7 -3
  5. package/dist/src/core/geminiChat.test.js.map +1 -1
  6. package/dist/src/core/geminiChat_network_retry.test.d.ts +6 -0
  7. package/dist/src/core/geminiChat_network_retry.test.js +195 -0
  8. package/dist/src/core/geminiChat_network_retry.test.js.map +1 -0
  9. package/dist/src/generated/git-commit.d.ts +2 -2
  10. package/dist/src/generated/git-commit.js +2 -2
  11. package/dist/src/index.d.ts +1 -0
  12. package/dist/src/index.js +1 -0
  13. package/dist/src/index.js.map +1 -1
  14. package/dist/src/services/chatRecordingService.d.ts +9 -0
  15. package/dist/src/services/chatRecordingService.js +30 -0
  16. package/dist/src/services/chatRecordingService.js.map +1 -1
  17. package/dist/src/services/sessionSummaryService.d.ts +28 -0
  18. package/dist/src/services/sessionSummaryService.js +131 -0
  19. package/dist/src/services/sessionSummaryService.js.map +1 -0
  20. package/dist/src/services/sessionSummaryService.test.d.ts +6 -0
  21. package/dist/src/services/sessionSummaryService.test.js +785 -0
  22. package/dist/src/services/sessionSummaryService.test.js.map +1 -0
  23. package/dist/src/services/sessionSummaryUtils.d.ts +11 -0
  24. package/dist/src/services/sessionSummaryUtils.js +61 -0
  25. package/dist/src/services/sessionSummaryUtils.js.map +1 -0
  26. package/dist/src/services/sessionSummaryUtils.test.d.ts +6 -0
  27. package/dist/src/services/sessionSummaryUtils.test.js +298 -0
  28. package/dist/src/services/sessionSummaryUtils.test.js.map +1 -0
  29. package/dist/src/services/shellExecutionService.d.ts +2 -0
  30. package/dist/src/services/shellExecutionService.js +31 -3
  31. package/dist/src/services/shellExecutionService.js.map +1 -1
  32. package/dist/src/services/shellExecutionService.test.js +80 -2
  33. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  34. package/dist/src/tools/web-fetch.js +12 -4
  35. package/dist/src/tools/web-fetch.js.map +1 -1
  36. package/dist/src/tools/web-fetch.test.js +1 -0
  37. package/dist/src/tools/web-fetch.test.js.map +1 -1
  38. package/dist/src/utils/errors.js +7 -2
  39. package/dist/src/utils/errors.js.map +1 -1
  40. package/dist/src/utils/errors.test.js +120 -1
  41. package/dist/src/utils/errors.test.js.map +1 -1
  42. package/dist/src/utils/fetch.d.ts +1 -1
  43. package/dist/src/utils/fetch.js +3 -3
  44. package/dist/src/utils/fetch.js.map +1 -1
  45. package/dist/src/utils/retry.d.ts +8 -0
  46. package/dist/src/utils/retry.js +8 -7
  47. package/dist/src/utils/retry.js.map +1 -1
  48. package/dist/src/utils/retry.test.js +33 -3
  49. package/dist/src/utils/retry.test.js.map +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +1 -1
  52. package/dist/google-gemini-cli-core-0.21.0-nightly.20251202.2d935b379.tgz +0 -0
@@ -0,0 +1,785 @@
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 { SessionSummaryService } from './sessionSummaryService.js';
8
+ describe('SessionSummaryService', () => {
9
+ let service;
10
+ let mockBaseLlmClient;
11
+ let mockGenerateContent;
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ vi.useFakeTimers();
15
+ // Setup mock BaseLlmClient with generateContent
16
+ mockGenerateContent = vi.fn().mockResolvedValue({
17
+ candidates: [
18
+ {
19
+ content: {
20
+ parts: [{ text: 'Add dark mode to the app' }],
21
+ },
22
+ },
23
+ ],
24
+ });
25
+ mockBaseLlmClient = {
26
+ generateContent: mockGenerateContent,
27
+ };
28
+ service = new SessionSummaryService(mockBaseLlmClient);
29
+ });
30
+ afterEach(() => {
31
+ vi.useRealTimers();
32
+ vi.restoreAllMocks();
33
+ });
34
+ describe('Basic Functionality', () => {
35
+ it('should generate summary for valid conversation', async () => {
36
+ const messages = [
37
+ {
38
+ id: '1',
39
+ timestamp: '2025-12-03T00:00:00Z',
40
+ type: 'user',
41
+ content: [{ text: 'How do I add dark mode to my app?' }],
42
+ },
43
+ {
44
+ id: '2',
45
+ timestamp: '2025-12-03T00:01:00Z',
46
+ type: 'gemini',
47
+ content: [
48
+ {
49
+ text: 'To add dark mode, you need to create a theme provider and toggle state...',
50
+ },
51
+ ],
52
+ },
53
+ ];
54
+ const summary = await service.generateSummary({ messages });
55
+ expect(summary).toBe('Add dark mode to the app');
56
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
57
+ expect(mockGenerateContent).toHaveBeenCalledWith(expect.objectContaining({
58
+ modelConfigKey: { model: 'summarizer-default' },
59
+ contents: expect.arrayContaining([
60
+ expect.objectContaining({
61
+ role: 'user',
62
+ parts: expect.arrayContaining([
63
+ expect.objectContaining({
64
+ text: expect.stringContaining('User: How do I add dark mode'),
65
+ }),
66
+ ]),
67
+ }),
68
+ ]),
69
+ promptId: 'session-summary-generation',
70
+ }));
71
+ });
72
+ it('should return null for empty messages array', async () => {
73
+ const summary = await service.generateSummary({ messages: [] });
74
+ expect(summary).toBeNull();
75
+ expect(mockGenerateContent).not.toHaveBeenCalled();
76
+ });
77
+ it('should return null when all messages have empty content', async () => {
78
+ const messages = [
79
+ {
80
+ id: '1',
81
+ timestamp: '2025-12-03T00:00:00Z',
82
+ type: 'user',
83
+ content: [{ text: ' ' }],
84
+ },
85
+ {
86
+ id: '2',
87
+ timestamp: '2025-12-03T00:01:00Z',
88
+ type: 'gemini',
89
+ content: [{ text: '' }],
90
+ },
91
+ ];
92
+ const summary = await service.generateSummary({ messages });
93
+ expect(summary).toBeNull();
94
+ expect(mockGenerateContent).not.toHaveBeenCalled();
95
+ });
96
+ it('should handle maxMessages limit correctly', async () => {
97
+ const messages = Array.from({ length: 30 }, (_, i) => ({
98
+ id: `${i}`,
99
+ timestamp: '2025-12-03T00:00:00Z',
100
+ type: i % 2 === 0 ? 'user' : 'gemini',
101
+ content: [{ text: `Message ${i}` }],
102
+ }));
103
+ await service.generateSummary({ messages, maxMessages: 10 });
104
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
105
+ const callArgs = mockGenerateContent.mock.calls[0][0];
106
+ const promptText = callArgs.contents[0].parts[0].text;
107
+ // Count how many messages appear in the prompt (should be 10)
108
+ const messageCount = (promptText.match(/Message \d+/g) || []).length;
109
+ expect(messageCount).toBe(10);
110
+ });
111
+ });
112
+ describe('Message Type Filtering', () => {
113
+ it('should include only user and gemini messages', async () => {
114
+ const messages = [
115
+ {
116
+ id: '1',
117
+ timestamp: '2025-12-03T00:00:00Z',
118
+ type: 'user',
119
+ content: [{ text: 'User message' }],
120
+ },
121
+ {
122
+ id: '2',
123
+ timestamp: '2025-12-03T00:01:00Z',
124
+ type: 'gemini',
125
+ content: [{ text: 'Gemini response' }],
126
+ },
127
+ ];
128
+ await service.generateSummary({ messages });
129
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
130
+ const callArgs = mockGenerateContent.mock.calls[0][0];
131
+ const promptText = callArgs.contents[0].parts[0].text;
132
+ expect(promptText).toContain('User: User message');
133
+ expect(promptText).toContain('Assistant: Gemini response');
134
+ });
135
+ it('should exclude info messages', async () => {
136
+ const messages = [
137
+ {
138
+ id: '1',
139
+ timestamp: '2025-12-03T00:00:00Z',
140
+ type: 'user',
141
+ content: [{ text: 'User message' }],
142
+ },
143
+ {
144
+ id: '2',
145
+ timestamp: '2025-12-03T00:01:00Z',
146
+ type: 'info',
147
+ content: [{ text: 'Info message should be excluded' }],
148
+ },
149
+ {
150
+ id: '3',
151
+ timestamp: '2025-12-03T00:02:00Z',
152
+ type: 'gemini',
153
+ content: [{ text: 'Gemini response' }],
154
+ },
155
+ ];
156
+ await service.generateSummary({ messages });
157
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
158
+ const callArgs = mockGenerateContent.mock.calls[0][0];
159
+ const promptText = callArgs.contents[0].parts[0].text;
160
+ expect(promptText).toContain('User: User message');
161
+ expect(promptText).toContain('Assistant: Gemini response');
162
+ expect(promptText).not.toContain('Info message');
163
+ });
164
+ it('should exclude error messages', async () => {
165
+ const messages = [
166
+ {
167
+ id: '1',
168
+ timestamp: '2025-12-03T00:00:00Z',
169
+ type: 'user',
170
+ content: [{ text: 'User message' }],
171
+ },
172
+ {
173
+ id: '2',
174
+ timestamp: '2025-12-03T00:01:00Z',
175
+ type: 'error',
176
+ content: [{ text: 'Error: something went wrong' }],
177
+ },
178
+ {
179
+ id: '3',
180
+ timestamp: '2025-12-03T00:02:00Z',
181
+ type: 'gemini',
182
+ content: [{ text: 'Gemini response' }],
183
+ },
184
+ ];
185
+ await service.generateSummary({ messages });
186
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
187
+ const callArgs = mockGenerateContent.mock.calls[0][0];
188
+ const promptText = callArgs.contents[0].parts[0].text;
189
+ expect(promptText).not.toContain('Error: something went wrong');
190
+ });
191
+ it('should exclude warning messages', async () => {
192
+ const messages = [
193
+ {
194
+ id: '1',
195
+ timestamp: '2025-12-03T00:00:00Z',
196
+ type: 'user',
197
+ content: [{ text: 'User message' }],
198
+ },
199
+ {
200
+ id: '2',
201
+ timestamp: '2025-12-03T00:01:00Z',
202
+ type: 'warning',
203
+ content: [{ text: 'Warning: deprecated API' }],
204
+ },
205
+ {
206
+ id: '3',
207
+ timestamp: '2025-12-03T00:02:00Z',
208
+ type: 'gemini',
209
+ content: [{ text: 'Gemini response' }],
210
+ },
211
+ ];
212
+ await service.generateSummary({ messages });
213
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
214
+ const callArgs = mockGenerateContent.mock.calls[0][0];
215
+ const promptText = callArgs.contents[0].parts[0].text;
216
+ expect(promptText).not.toContain('Warning: deprecated API');
217
+ });
218
+ it('should handle mixed message types correctly', async () => {
219
+ const messages = [
220
+ {
221
+ id: '1',
222
+ timestamp: '2025-12-03T00:00:00Z',
223
+ type: 'info',
224
+ content: [{ text: 'System info' }],
225
+ },
226
+ {
227
+ id: '2',
228
+ timestamp: '2025-12-03T00:01:00Z',
229
+ type: 'user',
230
+ content: [{ text: 'User question' }],
231
+ },
232
+ {
233
+ id: '3',
234
+ timestamp: '2025-12-03T00:02:00Z',
235
+ type: 'error',
236
+ content: [{ text: 'Error occurred' }],
237
+ },
238
+ {
239
+ id: '4',
240
+ timestamp: '2025-12-03T00:03:00Z',
241
+ type: 'gemini',
242
+ content: [{ text: 'Gemini answer' }],
243
+ },
244
+ {
245
+ id: '5',
246
+ timestamp: '2025-12-03T00:04:00Z',
247
+ type: 'warning',
248
+ content: [{ text: 'Warning message' }],
249
+ },
250
+ ];
251
+ await service.generateSummary({ messages });
252
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
253
+ const callArgs = mockGenerateContent.mock.calls[0][0];
254
+ const promptText = callArgs.contents[0].parts[0].text;
255
+ expect(promptText).toContain('User: User question');
256
+ expect(promptText).toContain('Assistant: Gemini answer');
257
+ expect(promptText).not.toContain('System info');
258
+ expect(promptText).not.toContain('Error occurred');
259
+ expect(promptText).not.toContain('Warning message');
260
+ });
261
+ it('should return null when only system messages present', async () => {
262
+ const messages = [
263
+ {
264
+ id: '1',
265
+ timestamp: '2025-12-03T00:00:00Z',
266
+ type: 'info',
267
+ content: [{ text: 'Info message' }],
268
+ },
269
+ {
270
+ id: '2',
271
+ timestamp: '2025-12-03T00:01:00Z',
272
+ type: 'error',
273
+ content: [{ text: 'Error message' }],
274
+ },
275
+ {
276
+ id: '3',
277
+ timestamp: '2025-12-03T00:02:00Z',
278
+ type: 'warning',
279
+ content: [{ text: 'Warning message' }],
280
+ },
281
+ ];
282
+ const summary = await service.generateSummary({ messages });
283
+ expect(summary).toBeNull();
284
+ expect(mockGenerateContent).not.toHaveBeenCalled();
285
+ });
286
+ });
287
+ describe('Timeout and Abort Handling', () => {
288
+ it('should timeout after specified duration', async () => {
289
+ // Mock implementation that respects abort signal
290
+ mockGenerateContent.mockImplementation(({ abortSignal }) => new Promise((resolve, reject) => {
291
+ const timeoutId = setTimeout(() => resolve({
292
+ candidates: [{ content: { parts: [{ text: 'Summary' }] } }],
293
+ }), 10000);
294
+ abortSignal?.addEventListener('abort', () => {
295
+ clearTimeout(timeoutId);
296
+ const abortError = new Error('This operation was aborted');
297
+ abortError.name = 'AbortError';
298
+ reject(abortError);
299
+ });
300
+ }));
301
+ const messages = [
302
+ {
303
+ id: '1',
304
+ timestamp: '2025-12-03T00:00:00Z',
305
+ type: 'user',
306
+ content: [{ text: 'Hello' }],
307
+ },
308
+ ];
309
+ const summaryPromise = service.generateSummary({
310
+ messages,
311
+ timeout: 100,
312
+ });
313
+ // Advance timers past the timeout to trigger abort
314
+ await vi.advanceTimersByTimeAsync(100);
315
+ const summary = await summaryPromise;
316
+ expect(summary).toBeNull();
317
+ });
318
+ it('should detect AbortError by name only (not message)', async () => {
319
+ const abortError = new Error('Different abort message');
320
+ abortError.name = 'AbortError';
321
+ mockGenerateContent.mockRejectedValue(abortError);
322
+ const messages = [
323
+ {
324
+ id: '1',
325
+ timestamp: '2025-12-03T00:00:00Z',
326
+ type: 'user',
327
+ content: [{ text: 'Hello' }],
328
+ },
329
+ ];
330
+ const summary = await service.generateSummary({ messages });
331
+ expect(summary).toBeNull();
332
+ // Should handle it gracefully without throwing
333
+ });
334
+ it('should handle API errors gracefully', async () => {
335
+ mockGenerateContent.mockRejectedValue(new Error('API Error'));
336
+ const messages = [
337
+ {
338
+ id: '1',
339
+ timestamp: '2025-12-03T00:00:00Z',
340
+ type: 'user',
341
+ content: [{ text: 'Hello' }],
342
+ },
343
+ ];
344
+ const summary = await service.generateSummary({ messages });
345
+ expect(summary).toBeNull();
346
+ });
347
+ it('should handle empty response from LLM', async () => {
348
+ mockGenerateContent.mockResolvedValue({
349
+ candidates: [
350
+ {
351
+ content: {
352
+ parts: [{ text: '' }],
353
+ },
354
+ },
355
+ ],
356
+ });
357
+ const messages = [
358
+ {
359
+ id: '1',
360
+ timestamp: '2025-12-03T00:00:00Z',
361
+ type: 'user',
362
+ content: [{ text: 'Hello' }],
363
+ },
364
+ ];
365
+ const summary = await service.generateSummary({ messages });
366
+ expect(summary).toBeNull();
367
+ });
368
+ });
369
+ describe('Text Processing', () => {
370
+ it('should clean newlines and extra whitespace', async () => {
371
+ mockGenerateContent.mockResolvedValue({
372
+ candidates: [
373
+ {
374
+ content: {
375
+ parts: [
376
+ {
377
+ text: 'Add dark mode\n\nto the app',
378
+ },
379
+ ],
380
+ },
381
+ },
382
+ ],
383
+ });
384
+ const messages = [
385
+ {
386
+ id: '1',
387
+ timestamp: '2025-12-03T00:00:00Z',
388
+ type: 'user',
389
+ content: [{ text: 'Hello' }],
390
+ },
391
+ ];
392
+ const summary = await service.generateSummary({ messages });
393
+ expect(summary).toBe('Add dark mode to the app');
394
+ });
395
+ it('should remove surrounding quotes', async () => {
396
+ mockGenerateContent.mockResolvedValue({
397
+ candidates: [
398
+ {
399
+ content: {
400
+ parts: [{ text: '"Add dark mode to the app"' }],
401
+ },
402
+ },
403
+ ],
404
+ });
405
+ const messages = [
406
+ {
407
+ id: '1',
408
+ timestamp: '2025-12-03T00:00:00Z',
409
+ type: 'user',
410
+ content: [{ text: 'Hello' }],
411
+ },
412
+ ];
413
+ const summary = await service.generateSummary({ messages });
414
+ expect(summary).toBe('Add dark mode to the app');
415
+ });
416
+ it('should handle messages longer than 500 chars', async () => {
417
+ const longMessage = 'a'.repeat(1000);
418
+ const messages = [
419
+ {
420
+ id: '1',
421
+ timestamp: '2025-12-03T00:00:00Z',
422
+ type: 'user',
423
+ content: [{ text: longMessage }],
424
+ },
425
+ {
426
+ id: '2',
427
+ timestamp: '2025-12-03T00:01:00Z',
428
+ type: 'gemini',
429
+ content: [{ text: 'Response' }],
430
+ },
431
+ ];
432
+ await service.generateSummary({ messages });
433
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
434
+ const callArgs = mockGenerateContent.mock.calls[0][0];
435
+ const promptText = callArgs.contents[0].parts[0].text;
436
+ // Should be truncated to ~500 chars + "..."
437
+ expect(promptText).toContain('...');
438
+ expect(promptText).not.toContain('a'.repeat(600));
439
+ });
440
+ it('should preserve important content in truncation', async () => {
441
+ const messages = [
442
+ {
443
+ id: '1',
444
+ timestamp: '2025-12-03T00:00:00Z',
445
+ type: 'user',
446
+ content: [{ text: 'How do I add dark mode?' }],
447
+ },
448
+ {
449
+ id: '2',
450
+ timestamp: '2025-12-03T00:01:00Z',
451
+ type: 'gemini',
452
+ content: [
453
+ {
454
+ text: 'Here is a detailed explanation...',
455
+ },
456
+ ],
457
+ },
458
+ ];
459
+ await service.generateSummary({ messages });
460
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
461
+ const callArgs = mockGenerateContent.mock.calls[0][0];
462
+ const promptText = callArgs.contents[0].parts[0].text;
463
+ // User question should be preserved
464
+ expect(promptText).toContain('User: How do I add dark mode?');
465
+ expect(promptText).toContain('Assistant: Here is a detailed explanation');
466
+ });
467
+ });
468
+ describe('Sliding Window Message Selection', () => {
469
+ it('should return all messages when fewer than 20 exist', async () => {
470
+ const messages = Array.from({ length: 5 }, (_, i) => ({
471
+ id: `${i}`,
472
+ timestamp: '2025-12-03T00:00:00Z',
473
+ type: i % 2 === 0 ? 'user' : 'gemini',
474
+ content: [{ text: `Message ${i}` }],
475
+ }));
476
+ await service.generateSummary({ messages });
477
+ const callArgs = mockGenerateContent.mock.calls[0][0];
478
+ const promptText = callArgs.contents[0].parts[0].text;
479
+ const messageCount = (promptText.match(/Message \d+/g) || []).length;
480
+ expect(messageCount).toBe(5);
481
+ });
482
+ it('should select first 10 + last 10 from 50 messages', async () => {
483
+ const messages = Array.from({ length: 50 }, (_, i) => ({
484
+ id: `${i}`,
485
+ timestamp: '2025-12-03T00:00:00Z',
486
+ type: i % 2 === 0 ? 'user' : 'gemini',
487
+ content: [{ text: `Message ${i}` }],
488
+ }));
489
+ await service.generateSummary({ messages });
490
+ const callArgs = mockGenerateContent.mock.calls[0][0];
491
+ const promptText = callArgs.contents[0].parts[0].text;
492
+ // Should include first 10
493
+ expect(promptText).toContain('Message 0');
494
+ expect(promptText).toContain('Message 9');
495
+ // Should skip middle
496
+ expect(promptText).not.toContain('Message 25');
497
+ // Should include last 10
498
+ expect(promptText).toContain('Message 40');
499
+ expect(promptText).toContain('Message 49');
500
+ const messageCount = (promptText.match(/Message \d+/g) || []).length;
501
+ expect(messageCount).toBe(20);
502
+ });
503
+ it('should return all messages when exactly 20 exist', async () => {
504
+ const messages = Array.from({ length: 20 }, (_, i) => ({
505
+ id: `${i}`,
506
+ timestamp: '2025-12-03T00:00:00Z',
507
+ type: i % 2 === 0 ? 'user' : 'gemini',
508
+ content: [{ text: `Message ${i}` }],
509
+ }));
510
+ await service.generateSummary({ messages });
511
+ const callArgs = mockGenerateContent.mock.calls[0][0];
512
+ const promptText = callArgs.contents[0].parts[0].text;
513
+ const messageCount = (promptText.match(/Message \d+/g) || []).length;
514
+ expect(messageCount).toBe(20);
515
+ });
516
+ it('should preserve message ordering in sliding window', async () => {
517
+ const messages = Array.from({ length: 30 }, (_, i) => ({
518
+ id: `${i}`,
519
+ timestamp: '2025-12-03T00:00:00Z',
520
+ type: i % 2 === 0 ? 'user' : 'gemini',
521
+ content: [{ text: `Message ${i}` }],
522
+ }));
523
+ await service.generateSummary({ messages });
524
+ const callArgs = mockGenerateContent.mock.calls[0][0];
525
+ const promptText = callArgs.contents[0].parts[0].text;
526
+ const matches = promptText.match(/Message (\d+)/g) || [];
527
+ const indices = matches.map((m) => parseInt(m.split(' ')[1], 10));
528
+ // Verify ordering is preserved
529
+ for (let i = 1; i < indices.length; i++) {
530
+ expect(indices[i]).toBeGreaterThan(indices[i - 1]);
531
+ }
532
+ });
533
+ it('should not count system messages when calculating window', async () => {
534
+ const messages = [
535
+ // First 10 user/gemini messages
536
+ ...Array.from({ length: 10 }, (_, i) => ({
537
+ id: `${i}`,
538
+ timestamp: '2025-12-03T00:00:00Z',
539
+ type: i % 2 === 0 ? 'user' : 'gemini',
540
+ content: [{ text: `Message ${i}` }],
541
+ })),
542
+ // System messages (should be filtered out)
543
+ {
544
+ id: 'info1',
545
+ timestamp: '2025-12-03T00:10:00Z',
546
+ type: 'info',
547
+ content: [{ text: 'Info' }],
548
+ },
549
+ {
550
+ id: 'warn1',
551
+ timestamp: '2025-12-03T00:11:00Z',
552
+ type: 'warning',
553
+ content: [{ text: 'Warning' }],
554
+ },
555
+ // Last 40 user/gemini messages
556
+ ...Array.from({ length: 40 }, (_, i) => ({
557
+ id: `${i + 10}`,
558
+ timestamp: '2025-12-03T00:12:00Z',
559
+ type: i % 2 === 0 ? 'user' : 'gemini',
560
+ content: [{ text: `Message ${i + 10}` }],
561
+ })),
562
+ ];
563
+ await service.generateSummary({ messages });
564
+ const callArgs = mockGenerateContent.mock.calls[0][0];
565
+ const promptText = callArgs.contents[0].parts[0].text;
566
+ // Should include early messages
567
+ expect(promptText).toContain('Message 0');
568
+ expect(promptText).toContain('Message 9');
569
+ // Should include late messages
570
+ expect(promptText).toContain('Message 40');
571
+ expect(promptText).toContain('Message 49');
572
+ // Should not include system messages
573
+ expect(promptText).not.toContain('Info');
574
+ expect(promptText).not.toContain('Warning');
575
+ });
576
+ });
577
+ describe('Edge Cases', () => {
578
+ it('should handle conversation with only user messages', async () => {
579
+ const messages = [
580
+ {
581
+ id: '1',
582
+ timestamp: '2025-12-03T00:00:00Z',
583
+ type: 'user',
584
+ content: [{ text: 'First question' }],
585
+ },
586
+ {
587
+ id: '2',
588
+ timestamp: '2025-12-03T00:01:00Z',
589
+ type: 'user',
590
+ content: [{ text: 'Second question' }],
591
+ },
592
+ ];
593
+ const summary = await service.generateSummary({ messages });
594
+ expect(summary).not.toBeNull();
595
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
596
+ });
597
+ it('should handle conversation with only gemini messages', async () => {
598
+ const messages = [
599
+ {
600
+ id: '1',
601
+ timestamp: '2025-12-03T00:00:00Z',
602
+ type: 'gemini',
603
+ content: [{ text: 'First response' }],
604
+ },
605
+ {
606
+ id: '2',
607
+ timestamp: '2025-12-03T00:01:00Z',
608
+ type: 'gemini',
609
+ content: [{ text: 'Second response' }],
610
+ },
611
+ ];
612
+ const summary = await service.generateSummary({ messages });
613
+ expect(summary).not.toBeNull();
614
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
615
+ });
616
+ it('should handle very long individual messages (>500 chars)', async () => {
617
+ const longMessage = `This is a very long message that contains a lot of text and definitely exceeds the 500 character limit. `.repeat(10);
618
+ const messages = [
619
+ {
620
+ id: '1',
621
+ timestamp: '2025-12-03T00:00:00Z',
622
+ type: 'user',
623
+ content: [{ text: longMessage }],
624
+ },
625
+ ];
626
+ await service.generateSummary({ messages });
627
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
628
+ const callArgs = mockGenerateContent.mock.calls[0][0];
629
+ const promptText = callArgs.contents[0].parts[0].text;
630
+ // Should contain the truncation marker
631
+ expect(promptText).toContain('...');
632
+ });
633
+ it('should handle messages with special characters', async () => {
634
+ const messages = [
635
+ {
636
+ id: '1',
637
+ timestamp: '2025-12-03T00:00:00Z',
638
+ type: 'user',
639
+ content: [
640
+ {
641
+ text: 'How to use <Component> with props={value} & state?',
642
+ },
643
+ ],
644
+ },
645
+ ];
646
+ const summary = await service.generateSummary({ messages });
647
+ expect(summary).not.toBeNull();
648
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
649
+ });
650
+ it('should handle malformed message content', async () => {
651
+ const messages = [
652
+ {
653
+ id: '1',
654
+ timestamp: '2025-12-03T00:00:00Z',
655
+ type: 'user',
656
+ content: [], // Empty parts array
657
+ },
658
+ {
659
+ id: '2',
660
+ timestamp: '2025-12-03T00:01:00Z',
661
+ type: 'gemini',
662
+ content: [{ text: 'Valid response' }],
663
+ },
664
+ ];
665
+ await service.generateSummary({ messages });
666
+ // Should handle gracefully and still process valid messages
667
+ expect(mockGenerateContent).toHaveBeenCalled();
668
+ });
669
+ });
670
+ describe('Internationalization Support', () => {
671
+ it('should preserve international characters (Chinese)', async () => {
672
+ mockGenerateContent.mockResolvedValue({
673
+ candidates: [
674
+ {
675
+ content: {
676
+ parts: [{ text: '添加深色模式到应用' }],
677
+ },
678
+ },
679
+ ],
680
+ });
681
+ const messages = [
682
+ {
683
+ id: '1',
684
+ timestamp: '2025-12-03T00:00:00Z',
685
+ type: 'user',
686
+ content: [{ text: 'How do I add dark mode?' }],
687
+ },
688
+ ];
689
+ const summary = await service.generateSummary({ messages });
690
+ expect(summary).toBe('添加深色模式到应用');
691
+ });
692
+ it('should preserve international characters (Arabic)', async () => {
693
+ mockGenerateContent.mockResolvedValue({
694
+ candidates: [
695
+ {
696
+ content: {
697
+ parts: [{ text: 'إضافة الوضع الداكن' }],
698
+ },
699
+ },
700
+ ],
701
+ });
702
+ const messages = [
703
+ {
704
+ id: '1',
705
+ timestamp: '2025-12-03T00:00:00Z',
706
+ type: 'user',
707
+ content: [{ text: 'How do I add dark mode?' }],
708
+ },
709
+ ];
710
+ const summary = await service.generateSummary({ messages });
711
+ expect(summary).toBe('إضافة الوضع الداكن');
712
+ });
713
+ it('should preserve accented characters', async () => {
714
+ mockGenerateContent.mockResolvedValue({
715
+ candidates: [
716
+ {
717
+ content: {
718
+ parts: [{ text: 'Añadir modo oscuro à la aplicación' }],
719
+ },
720
+ },
721
+ ],
722
+ });
723
+ const messages = [
724
+ {
725
+ id: '1',
726
+ timestamp: '2025-12-03T00:00:00Z',
727
+ type: 'user',
728
+ content: [{ text: 'How do I add dark mode?' }],
729
+ },
730
+ ];
731
+ const summary = await service.generateSummary({ messages });
732
+ expect(summary).toBe('Añadir modo oscuro à la aplicación');
733
+ });
734
+ it('should preserve emojis in summaries', async () => {
735
+ mockGenerateContent.mockResolvedValue({
736
+ candidates: [
737
+ {
738
+ content: {
739
+ parts: [{ text: '🌙 Add dark mode 🎨 to the app ✨' }],
740
+ },
741
+ },
742
+ ],
743
+ });
744
+ const messages = [
745
+ {
746
+ id: '1',
747
+ timestamp: '2025-12-03T00:00:00Z',
748
+ type: 'user',
749
+ content: [{ text: 'How do I add dark mode?' }],
750
+ },
751
+ ];
752
+ const summary = await service.generateSummary({ messages });
753
+ // Emojis are preserved
754
+ expect(summary).toBe('🌙 Add dark mode 🎨 to the app ✨');
755
+ expect(summary).toContain('🌙');
756
+ expect(summary).toContain('🎨');
757
+ expect(summary).toContain('✨');
758
+ });
759
+ it('should preserve zero-width characters for language rendering', async () => {
760
+ // Arabic with Zero-Width Joiner (ZWJ) for proper ligatures
761
+ mockGenerateContent.mockResolvedValue({
762
+ candidates: [
763
+ {
764
+ content: {
765
+ parts: [{ text: 'كلمة\u200Dمتصلة' }], // Contains ZWJ
766
+ },
767
+ },
768
+ ],
769
+ });
770
+ const messages = [
771
+ {
772
+ id: '1',
773
+ timestamp: '2025-12-03T00:00:00Z',
774
+ type: 'user',
775
+ content: [{ text: 'Test' }],
776
+ },
777
+ ];
778
+ const summary = await service.generateSummary({ messages });
779
+ // ZWJ is preserved (it's not considered whitespace)
780
+ expect(summary).toBe('كلمة\u200Dمتصلة');
781
+ expect(summary).toContain('\u200D'); // ZWJ should be preserved
782
+ });
783
+ });
784
+ });
785
+ //# sourceMappingURL=sessionSummaryService.test.js.map