"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const openai_service_1 = require("../../../src/services/ai/openai.service"); const openai_1 = __importDefault(require("openai")); // Mock OpenAI jest.mock('openai'); describe('OpenAIService', () => { let service; let mockClient; let mockConfig; beforeEach(() => { // Reset mocks jest.clearAllMocks(); // Create mock config mockConfig = { openai: { apiKey: 'test-api-key', model: 'gpt-4-turbo', maxTokens: 4096, temperature: 0.7, retryAttempts: 3, retryDelayMs: 100, timeout: 30000, }, langchain: { verbose: false, cacheEnabled: true, cacheTTL: 3600, maxConcurrency: 5, memoryBufferSize: 10, }, rateLimit: { maxRequestsPerMinute: 60, maxTokensPerMinute: 90000, maxRequestsPerDay: 10000, }, monitoring: { trackTokenUsage: true, logLevel: 'info', metricsEnabled: true, }, }; // Create service instance service = new openai_service_1.OpenAIService(mockConfig); // Get mock client instance mockClient = openai_1.default.mock.instances[0]; }); describe('complete', () => { it('should successfully complete a chat request', async () => { // Mock successful response const mockResponse = { id: 'test-id', object: 'chat.completion', created: Date.now(), model: 'gpt-4-turbo', choices: [{ index: 0, message: { role: 'assistant', content: 'Test response', }, finish_reason: 'stop', }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30, }, }; mockClient.chat = { completions: { create: jest.fn().mockResolvedValue(mockResponse), }, }; const result = await service.complete([ { role: 'user', content: 'Test message' }, ]); expect(result).toEqual({ content: 'Test response', promptTokens: 10, completionTokens: 20, totalTokens: 30, model: 'gpt-4-turbo', finishReason: 'stop', executionTimeMs: expect.any(Number), }); expect(mockClient.chat.completions.create).toHaveBeenCalledWith({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: 'Test message' }], temperature: 0.7, max_tokens: 4096, top_p: undefined, frequency_penalty: undefined, presence_penalty: undefined, stop: undefined, response_format: undefined, }); }); it('should include system prompt when provided', async () => { const mockResponse = { choices: [{ message: { content: 'Response' }, finish_reason: 'stop', }], usage: { prompt_tokens: 15, completion_tokens: 10, total_tokens: 25, }, model: 'gpt-4-turbo', }; mockClient.chat = { completions: { create: jest.fn().mockResolvedValue(mockResponse), }, }; await service.complete([{ role: 'user', content: 'Test' }], { systemPrompt: 'You are a helpful assistant' }); expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.objectContaining({ messages: [ { role: 'system', content: 'You are a helpful assistant' }, { role: 'user', content: 'Test' }, ], })); }); it('should handle JSON response format', async () => { const mockResponse = { choices: [{ message: { content: '{"key": "value"}' }, finish_reason: 'stop', }], usage: { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25, }, model: 'gpt-4-turbo', }; mockClient.chat = { completions: { create: jest.fn().mockResolvedValue(mockResponse), }, }; await service.complete([{ role: 'user', content: 'Return JSON' }], { responseFormat: 'json' }); expect(mockClient.chat.completions.create).toHaveBeenCalledWith(expect.objectContaining({ response_format: { type: 'json_object' }, })); }); }); describe('retry logic', () => { it('should retry on rate limit error (429)', async () => { const mockError = new Error('Rate limit exceeded'); mockError.status = 429; const mockResponse = { choices: [{ message: { content: 'Success after retry' }, finish_reason: 'stop', }], usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20, }, model: 'gpt-4-turbo', }; mockClient.chat = { completions: { create: jest.fn() .mockRejectedValueOnce(mockError) .mockResolvedValueOnce(mockResponse), }, }; const result = await service.complete([ { role: 'user', content: 'Test' }, ]); expect(result.content).toBe('Success after retry'); expect(mockClient.chat.completions.create).toHaveBeenCalledTimes(2); }); it('should retry on server error (500+)', async () => { const mockError = new Error('Internal server error'); mockError.status = 500; const mockResponse = { choices: [{ message: { content: 'Success' }, finish_reason: 'stop', }], usage: { prompt_tokens: 5, completion_tokens: 5, total_tokens: 10, }, model: 'gpt-4-turbo', }; mockClient.chat = { completions: { create: jest.fn() .mockRejectedValueOnce(mockError) .mockRejectedValueOnce(mockError) .mockResolvedValueOnce(mockResponse), }, }; const result = await service.complete([ { role: 'user', content: 'Test' }, ]); expect(result.content).toBe('Success'); expect(mockClient.chat.completions.create).toHaveBeenCalledTimes(3); }); it('should not retry on client error (400)', async () => { const mockError = new Error('Bad request'); mockError.status = 400; mockClient.chat = { completions: { create: jest.fn().mockRejectedValue(mockError), }, }; await expect(service.complete([ { role: 'user', content: 'Test' }, ])).rejects.toThrow(openai_service_1.OpenAIServiceError); expect(mockClient.chat.completions.create).toHaveBeenCalledTimes(1); }); it('should respect retry limit', async () => { const mockError = new Error('Server error'); mockError.status = 500; mockClient.chat = { completions: { create: jest.fn().mockRejectedValue(mockError), }, }; await expect(service.complete([ { role: 'user', content: 'Test' }, ])).rejects.toThrow(openai_service_1.OpenAIServiceError); // Should attempt initial + 3 retries = 4 total expect(mockClient.chat.completions.create).toHaveBeenCalledTimes(4); }); it('should handle retry-after header', async () => { const mockError = new Error('Rate limited'); mockError.status = 429; mockError.headers = { 'retry-after': '2' }; const mockResponse = { choices: [{ message: { content: 'Success' }, finish_reason: 'stop', }], usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20, }, model: 'gpt-4-turbo', }; mockClient.chat = { completions: { create: jest.fn() .mockRejectedValueOnce(mockError) .mockResolvedValueOnce(mockResponse), }, }; const startTime = Date.now(); await service.complete([{ role: 'user', content: 'Test' }]); const elapsed = Date.now() - startTime; // Should wait at least 2000ms due to retry-after header expect(elapsed).toBeGreaterThanOrEqual(2000); }); }); describe('streamComplete', () => { it('should handle streaming responses', async () => { const chunks = [ { choices: [{ delta: { content: 'Hello' } }] }, { choices: [{ delta: { content: ' world' } }] }, { choices: [{ finish_reason: 'stop' }] }, ]; const mockStream = { [Symbol.asyncIterator]: async function* () { for (const chunk of chunks) { yield chunk; } }, }; mockClient.chat = { completions: { create: jest.fn().mockResolvedValue(mockStream), }, }; const receivedChunks = []; const result = await service.streamComplete([{ role: 'user', content: 'Test' }], {}, (chunk) => receivedChunks.push(chunk)); expect(result.content).toBe('Hello world'); expect(result.finishReason).toBe('stop'); expect(receivedChunks).toEqual(['Hello', ' world']); }); }); describe('usage statistics', () => { it('should track token usage', async () => { const mockResponse1 = { choices: [{ message: { content: 'Response 1' }, finish_reason: 'stop', }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30, }, model: 'gpt-4-turbo', }; const mockResponse2 = { choices: [{ message: { content: 'Response 2' }, finish_reason: 'stop', }], usage: { prompt_tokens: 15, completion_tokens: 25, total_tokens: 40, }, model: 'gpt-4-turbo', }; mockClient.chat = { completions: { create: jest.fn() .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2), }, }; await service.complete([{ role: 'user', content: 'Test 1' }]); await service.complete([{ role: 'user', content: 'Test 2' }]); const stats = service.getUsageStats(); expect(stats.requestCount).toBe(2); expect(stats.totalTokensUsed).toBe(70); expect(stats.estimatedCost).toBeCloseTo(0.0014, 4); }); it('should reset usage statistics', async () => { const mockResponse = { choices: [{ message: { content: 'Response' }, finish_reason: 'stop', }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30, }, model: 'gpt-4-turbo', }; mockClient.chat = { completions: { create: jest.fn().mockResolvedValue(mockResponse), }, }; await service.complete([{ role: 'user', content: 'Test' }]); let stats = service.getUsageStats(); expect(stats.requestCount).toBe(1); expect(stats.totalTokensUsed).toBe(30); service.resetUsageStats(); stats = service.getUsageStats(); expect(stats.requestCount).toBe(0); expect(stats.totalTokensUsed).toBe(0); }); }); describe('testConnection', () => { it('should return true when connection is successful', async () => { const mockResponse = { choices: [{ message: { content: 'OK' }, finish_reason: 'stop', }], usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7, }, model: 'gpt-4-turbo', }; mockClient.chat = { completions: { create: jest.fn().mockResolvedValue(mockResponse), }, }; const result = await service.testConnection(); expect(result).toBe(true); }); it('should return false when connection fails', async () => { mockClient.chat = { completions: { create: jest.fn().mockRejectedValue(new Error('Connection failed')), }, }; const result = await service.testConnection(); expect(result).toBe(false); }); }); describe('error handling', () => { it('should wrap errors with OpenAIServiceError', async () => { const originalError = new Error('API error'); originalError.status = 400; originalError.code = 'invalid_request'; mockClient.chat = { completions: { create: jest.fn().mockRejectedValue(originalError), }, }; try { await service.complete([{ role: 'user', content: 'Test' }]); fail('Should have thrown error'); } catch (error) { expect(error).toBeInstanceOf(openai_service_1.OpenAIServiceError); expect(error.message).toContain('OpenAI API error'); expect(error.code).toBe('invalid_request'); expect(error.statusCode).toBe(400); expect(error.retryable).toBe(false); } }); it('should identify retryable errors correctly', async () => { const retryableErrors = [ { status: 429, expected: true }, // Rate limit { status: 500, expected: true }, // Server error { status: 503, expected: true }, // Service unavailable { code: 'ECONNRESET', expected: true }, // Network error { status: 400, expected: false }, // Client error { status: 401, expected: false }, // Unauthorized ]; for (const { status, code, expected } of retryableErrors) { const error = new Error('Test error'); if (status) error.status = status; if (code) error.code = code; mockClient.chat = { completions: { create: jest.fn().mockRejectedValue(error), }, }; try { await service.complete([{ role: 'user', content: 'Test' }]); } catch (error) { expect(error.retryable).toBe(expected); } } }); }); }); //# sourceMappingURL=openai.service.test.js.map