Files
gemini-cli/packages/core/src/services/sessionLearningsService.test.ts
T

183 lines
5.6 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SessionLearningsService } from './sessionLearningsService.js';
import type { Config } from '../config/config.js';
import type { GenerateContentResponse } from '@google/genai';
import fs from 'node:fs/promises';
import path from 'node:path';
describe('SessionLearningsService', () => {
let service: SessionLearningsService;
let mockConfig: unknown;
let mockRecordingService: any;
let mockGeminiClient: any;
let mockContentGenerator: any;
let mockGenerateContent: any;
beforeEach(() => {
vi.clearAllMocks();
mockGenerateContent = vi.fn().mockImplementation((_params, promptId) => {
if (promptId === 'session-learnings-generation') {
return Promise.resolve({
candidates: [
{
content: {
parts: [{ text: '# Session Learnings\nSummary text here.' }],
},
},
],
} as unknown as GenerateContentResponse);
} else if (promptId === 'session-summary-generation') {
return Promise.resolve({
candidates: [
{
content: {
parts: [{ text: 'Mock Session Title' }],
},
},
],
} as unknown as GenerateContentResponse);
}
return Promise.reject(new Error(`Unexpected promptId: ${promptId}`));
});
mockContentGenerator = {
generateContent: mockGenerateContent,
};
mockRecordingService = {
getConversation: vi.fn().mockReturnValue({
messages: [
{ type: 'user', content: [{ text: 'Question' }] },
{ type: 'gemini', content: [{ text: 'Answer' }] },
],
}),
};
mockGeminiClient = {
getChatRecordingService: () => mockRecordingService,
};
mockConfig = {
isSessionLearningsEnabled: vi.fn().mockReturnValue(true),
getSessionLearningsOutputPath: vi.fn().mockReturnValue(undefined),
getGeminiClient: () => mockGeminiClient,
getContentGenerator: () => mockContentGenerator,
getWorkingDir: () => '/mock/cwd',
getActiveModel: () => 'gemini-1.5-flash',
getModel: () => 'gemini-1.5-flash',
isInteractive: () => true,
setActiveModel: vi.fn(),
getUserTier: () => 'free',
getContentGeneratorConfig: () => ({ authType: 'apiKey' }),
getModelAvailabilityService: () => ({
selectFirstAvailable: (models: string[]) => ({
selectedModel: models[0],
}),
consumeStickyAttempt: vi.fn(),
markHealthy: vi.fn(),
}),
modelConfigService: {
getResolvedConfig: vi
.fn()
.mockReturnValue({ model: 'gemini-1.5-flash', config: {} }),
},
};
service = new SessionLearningsService(mockConfig as Config);
vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined);
vi.spyOn(fs, 'mkdir').mockResolvedValue(undefined as any);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should generate and save learnings with descriptive filename', async () => {
const dateStr = new Date().toISOString().split('T')[0];
await service.generateAndSaveLearnings();
expect(mockGenerateContent).toHaveBeenCalledTimes(2);
expect(fs.writeFile).toHaveBeenCalledWith(
path.join('/mock/cwd', `learnings-Mock-Session-Title-${dateStr}.md`),
'# Session Learnings\nSummary text here.',
'utf-8',
);
});
it('should use custom output path if configured', async () => {
const dateStr = new Date().toISOString().split('T')[0];
(mockConfig as any).getSessionLearningsOutputPath.mockReturnValue(
'custom/path',
);
await service.generateAndSaveLearnings();
expect(fs.mkdir).toHaveBeenCalledWith(
path.join('/mock/cwd', 'custom/path'),
{ recursive: true },
);
expect(fs.writeFile).toHaveBeenCalledWith(
path.join(
'/mock/cwd',
'custom/path',
`learnings-Mock-Session-Title-${dateStr}.md`,
),
'# Session Learnings\nSummary text here.',
'utf-8',
);
});
it('should use absolute output path if configured', async () => {
const dateStr = new Date().toISOString().split('T')[0];
(mockConfig as any).getSessionLearningsOutputPath.mockReturnValue(
'/absolute/path',
);
await service.generateAndSaveLearnings();
expect(fs.mkdir).toHaveBeenCalledWith('/absolute/path', {
recursive: true,
});
expect(fs.writeFile).toHaveBeenCalledWith(
path.join('/absolute/path', `learnings-Mock-Session-Title-${dateStr}.md`),
'# Session Learnings\nSummary text here.',
'utf-8',
);
});
it('should not generate learnings if disabled', async () => {
(mockConfig as any).isSessionLearningsEnabled.mockReturnValue(false);
await service.generateAndSaveLearnings();
expect(mockGenerateContent).not.toHaveBeenCalled();
expect(fs.writeFile).not.toHaveBeenCalled();
});
it('should not generate learnings if not enough messages', async () => {
mockRecordingService.getConversation.mockReturnValue({
messages: [{ type: 'user', content: [{ text: 'Single message' }] }],
});
await service.generateAndSaveLearnings();
expect(mockGenerateContent).not.toHaveBeenCalled();
});
it('should handle errors gracefully', async () => {
mockGenerateContent.mockRejectedValue(new Error('LLM Error'));
// Should not throw
await expect(service.generateAndSaveLearnings()).resolves.not.toThrow();
});
});