mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-05 06:31:44 -07:00
183 lines
5.6 KiB
TypeScript
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();
|
|
});
|
|
});
|