UNPKG

lamplighter-mcp

Version:

An intelligent context engine for AI-assisted software development

274 lines (229 loc) 9.91 kB
import 'openai/shims/node'; // Add node fetch shim import { AIService, IAIService, AIConfig } from '../../src/services/aiService'; import OpenAI from 'openai'; // --- Mocks --- // Mock OpenAI jest.mock('openai'); const mockedOpenAI = jest.mocked(OpenAI); const mockCreate = jest.fn(); mockedOpenAI.mockImplementation(() => ({ chat: { completions: { create: mockCreate, }, }, } as unknown as OpenAI)); // Mock fetch for Google Gemini global.fetch = jest.fn(); const mockedFetch = jest.mocked(global.fetch); // --- Environment Variable Management --- const originalEnv = process.env; beforeEach(() => { // Reset mocks and environment variables before each test jest.clearAllMocks(); process.env = { ...originalEnv }; // Start with original env // Set default required keys for tests, can be overridden process.env.OPENAI_API_KEY = 'test-openai-key'; process.env.GOOGLE_API_KEY = 'test-google-key'; }); afterAll(() => { // Restore original environment variables process.env = originalEnv; }); // --- Test Suites --- describe('AIService Factory (getInstance)', () => { beforeEach(() => { // Reset the singleton and mocks before each test (AIService as any).instance = undefined; mockedOpenAI.mockClear(); }); it('should return OpenAIService by default', () => { delete process.env.AI_PROVIDER; const instance = AIService.getInstance(); // Check if the instance is an OpenAIService (difficult directly, check constructor side effect) expect(mockedOpenAI).toHaveBeenCalled(); }); it('should return OpenAIService when AI_PROVIDER is openai', () => { process.env.AI_PROVIDER = 'openai'; const instance = AIService.getInstance(); expect(mockedOpenAI).toHaveBeenCalled(); }); it('should return GoogleGeminiService when AI_PROVIDER is google', () => { process.env.AI_PROVIDER = 'google'; const instance = AIService.getInstance(); // Check constructor side-effect (no direct OpenAI mock call) expect(mockedOpenAI).not.toHaveBeenCalled(); // Check if it implements IAIService (basic check) expect(instance.generateText).toBeDefined(); }); it('should return the same instance on subsequent calls (singleton)', () => { process.env.AI_PROVIDER = 'google'; const instance1 = AIService.getInstance(); const instance2 = AIService.getInstance(); expect(instance1).toBe(instance2); }); it('should re-initialize if instance is manually reset (for testing purposes)', () => { // This test demonstrates how to reset the singleton if needed between test suites mockedOpenAI.mockClear(); // Clear mock counts first process.env.AI_PROVIDER = 'openai'; const instance1 = AIService.getInstance(); expect(mockedOpenAI).toHaveBeenCalledTimes(1); // Manually reset internal state (AIService as any).instance = undefined; process.env.AI_PROVIDER = 'google'; const instance2 = AIService.getInstance(); expect(mockedOpenAI).toHaveBeenCalledTimes(1); // OpenAI should not be called again expect(instance1).not.toBe(instance2); }); }); describe('OpenAIService', () => { let openAIService: IAIService; beforeEach(() => { // Ensure we get an OpenAIService instance (AIService as any).instance = undefined; // Reset singleton process.env.AI_PROVIDER = 'openai'; openAIService = AIService.getInstance(); // Reset OpenAI mock function calls specifically for this suite mockCreate.mockClear(); }); it('should throw if OPENAI_API_KEY is missing', () => { delete process.env.OPENAI_API_KEY; (AIService as any).instance = undefined; // Reset to force re-creation expect(() => AIService.getInstance()).toThrow('OpenAI API key is required'); }); it('should call OpenAI create with correct parameters', async () => { const prompt = 'Test prompt'; const config: AIConfig = { temperature: 0.5, maxTokens: 100 }; const mockResponse = { choices: [{ message: { content: 'OpenAI response' } }] }; mockCreate.mockResolvedValue(mockResponse); await openAIService.generateText(prompt, config); expect(mockCreate).toHaveBeenCalledTimes(1); expect(mockCreate).toHaveBeenCalledWith({ model: process.env.AI_MODEL || 'gpt-4', // Check default or env model messages: [{ role: 'user', content: prompt }], temperature: config.temperature, max_tokens: config.maxTokens, top_p: 1, // Default top_p }); }); it('should use default config values if not provided', async () => { const prompt = 'Test prompt defaults'; const mockResponse = { choices: [{ message: { content: 'Default response' } }] }; mockCreate.mockResolvedValue(mockResponse); await openAIService.generateText(prompt); // No config provided expect(mockCreate).toHaveBeenCalledTimes(1); expect(mockCreate).toHaveBeenCalledWith({ model: process.env.AI_MODEL || 'gpt-4', messages: [{ role: 'user', content: prompt }], temperature: 0.7, // Default temperature max_tokens: undefined, // Default max_tokens top_p: 1, // Default top_p }); }); it('should return the content from the response', async () => { const expectedContent = 'Successful OpenAI response'; const mockResponse = { choices: [{ message: { content: expectedContent } }] }; mockCreate.mockResolvedValue(mockResponse); const result = await openAIService.generateText('Get response'); expect(result).toBe(expectedContent); }); it('should handle OpenAI API errors', async () => { const apiError = new Error('Invalid API Key'); mockCreate.mockRejectedValue(apiError); await expect(openAIService.generateText('Trigger error')).rejects.toThrow( `Failed to generate text with OpenAI: ${apiError.message}` ); }); }); describe('GoogleGeminiService', () => { let googleService: IAIService; beforeEach(() => { // Ensure we get a GoogleGeminiService instance (AIService as any).instance = undefined; // Reset singleton process.env.AI_PROVIDER = 'google'; googleService = AIService.getInstance(); // Reset fetch mock specifically for this suite mockedFetch.mockClear(); }); it('should throw if GOOGLE_API_KEY is missing', () => { delete process.env.GOOGLE_API_KEY; (AIService as any).instance = undefined; // Reset to force re-creation expect(() => AIService.getInstance()).toThrow('Google API key is required'); }); it('should call fetch with correct parameters', async () => { const prompt = 'Google test prompt'; const config: AIConfig = { temperature: 0.6, maxTokens: 150, topP: 0.9 }; const expectedContent = 'Google response'; const mockResponse = { candidates: [{ content: { parts: [{ text: expectedContent }] } }] }; mockedFetch.mockResolvedValue({ ok: true, json: async () => mockResponse, text: async () => JSON.stringify(mockResponse), } as Response); await googleService.generateText(prompt, config); expect(mockedFetch).toHaveBeenCalledTimes(1); const fetchCall = mockedFetch.mock.calls[0]; const url = fetchCall[0]; const options = fetchCall[1]; const expectedModel = process.env.AI_MODEL || 'gemini-pro'; expect(url).toContain(`https://generativelanguage.googleapis.com/v1/models/${expectedModel}:generateContent`); expect(url).toContain(`key=${process.env.GOOGLE_API_KEY}`); expect(options?.method).toBe('POST'); expect(options?.headers).toEqual({ 'Content-Type': 'application/json' }); expect(JSON.parse(options?.body as string)).toEqual({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: config.temperature, topP: config.topP, maxOutputTokens: config.maxTokens, }, }); }); it('should use default config values if not provided', async () => { const prompt = 'Google defaults'; const mockResponse = { candidates: [{ content: { parts: [{ text: 'Default google' }] } }] }; mockedFetch.mockResolvedValue({ ok: true, json: async () => mockResponse, text: async () => JSON.stringify(mockResponse), } as Response); await googleService.generateText(prompt); // No config expect(mockedFetch).toHaveBeenCalledTimes(1); const options = mockedFetch.mock.calls[0][1]; expect(JSON.parse(options?.body as string).generationConfig).toEqual({ temperature: 0.7, // Default temperature topP: 1, // Default topP maxOutputTokens: 1024, // Default maxOutputTokens }); }); it('should return the content from the response', async () => { const expectedContent = 'Successful Google response'; const mockResponse = { candidates: [{ content: { parts: [{ text: expectedContent }] } }] }; mockedFetch.mockResolvedValue({ ok: true, json: async () => mockResponse, text: async () => JSON.stringify(mockResponse), } as Response); const result = await googleService.generateText('Get response'); expect(result).toBe(expectedContent); }); it('should handle Google API errors (non-OK response)', async () => { const errorText = 'Invalid API key or resource exhausted.'; mockedFetch.mockResolvedValue({ ok: false, status: 400, json: async () => ({ error: { message: errorText } }), text: async () => errorText, } as Response); await expect(googleService.generateText('Trigger error')).rejects.toThrow( `Failed to generate text with Google Gemini: Google API error: 400 ${errorText}` ); }); it('should handle network errors during fetch', async () => { const networkError = new Error('Fetch failed'); mockedFetch.mockRejectedValue(networkError); await expect(googleService.generateText('Trigger network error')).rejects.toThrow( `Failed to generate text with Google Gemini: ${networkError.message}` ); }); });