From 6b6322798ef9eb60835c3a9ab08c1900aa2240cc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 9 Oct 2025 12:51:11 +0300 Subject: [PATCH] test(server): protected session timeout crashing on LLM tests --- apps/server/src/routes/api/llm.spec.ts | 6 +- .../services/llm/ai_service_manager.spec.ts | 136 +++++++------- .../llm/chat/rest_chat_service.spec.ts | 18 +- .../llm/config/configuration_helpers.spec.ts | 168 +++++++++--------- .../llm/providers/anthropic_service.spec.ts | 90 +++++----- .../llm/providers/model_selection.spec.ts | 24 +-- .../llm/providers/ollama_service.spec.ts | 120 +++++++------ .../llm/providers/openai_service.spec.ts | 52 +++--- 8 files changed, 323 insertions(+), 291 deletions(-) diff --git a/apps/server/src/routes/api/llm.spec.ts b/apps/server/src/routes/api/llm.spec.ts index e14012893..f8afddfa6 100644 --- a/apps/server/src/routes/api/llm.spec.ts +++ b/apps/server/src/routes/api/llm.spec.ts @@ -72,7 +72,11 @@ vi.mock("../../services/options.js", () => ({ getOptionMap: vi.fn(() => new Map()), createOption: vi.fn(), getOption: vi.fn(() => '0'), - getOptionOrNull: vi.fn(() => null) + getOptionOrNull: vi.fn(() => null), + getOptionInt: vi.fn(name => { + if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER; + return 0; + }) } })); diff --git a/apps/server/src/services/llm/ai_service_manager.spec.ts b/apps/server/src/services/llm/ai_service_manager.spec.ts index c31d90d26..14305cf6b 100644 --- a/apps/server/src/services/llm/ai_service_manager.spec.ts +++ b/apps/server/src/services/llm/ai_service_manager.spec.ts @@ -12,7 +12,11 @@ import type { AIService, ChatCompletionOptions, Message } from './ai_interface.j vi.mock('../options.js', () => ({ default: { getOption: vi.fn(), - getOptionBool: vi.fn() + getOptionBool: vi.fn(), + getOptionInt: vi.fn(name => { + if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER; + return 0; + }) } })); @@ -110,26 +114,26 @@ describe('AIServiceManager', () => { describe('getSelectedProviderAsync', () => { it('should return the selected provider', async () => { vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); - + const result = await manager.getSelectedProviderAsync(); - + expect(result).toBe('openai'); expect(configHelpers.getSelectedProvider).toHaveBeenCalled(); }); it('should return null if no provider is selected', async () => { vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null); - + const result = await manager.getSelectedProviderAsync(); - + expect(result).toBeNull(); }); it('should handle errors and return null', async () => { vi.mocked(configHelpers.getSelectedProvider).mockRejectedValueOnce(new Error('Config error')); - + const result = await manager.getSelectedProviderAsync(); - + expect(result).toBeNull(); }); }); @@ -141,9 +145,9 @@ describe('AIServiceManager', () => { errors: [], warnings: [] }); - + const result = await manager.validateConfiguration(); - + expect(result).toBeNull(); }); @@ -153,9 +157,9 @@ describe('AIServiceManager', () => { errors: ['Missing API key', 'Invalid model'], warnings: [] }); - + const result = await manager.validateConfiguration(); - + expect(result).toContain('There are issues with your AI configuration'); expect(result).toContain('Missing API key'); expect(result).toContain('Invalid model'); @@ -167,9 +171,9 @@ describe('AIServiceManager', () => { errors: [], warnings: ['Model not optimal'] }); - + const result = await manager.validateConfiguration(); - + expect(result).toBeNull(); }); }); @@ -178,21 +182,21 @@ describe('AIServiceManager', () => { it('should create and return the selected provider service', async () => { vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); - + const mockService = { isAvailable: vi.fn().mockReturnValue(true), generateChatCompletion: vi.fn() }; vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any); - + const result = await manager.getOrCreateAnyService(); - + expect(result).toBe(mockService); }); it('should throw error if no provider is selected', async () => { vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null); - + await expect(manager.getOrCreateAnyService()).rejects.toThrow( 'No AI provider is selected' ); @@ -201,7 +205,7 @@ describe('AIServiceManager', () => { it('should throw error if selected provider is not available', async () => { vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key - + await expect(manager.getOrCreateAnyService()).rejects.toThrow( 'Selected AI provider (openai) is not available' ); @@ -211,17 +215,17 @@ describe('AIServiceManager', () => { describe('isAnyServiceAvailable', () => { it('should return true if any provider is available', () => { vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); - + const result = manager.isAnyServiceAvailable(); - + expect(result).toBe(true); }); it('should return false if no providers are available', () => { vi.mocked(options.getOption).mockReturnValue(''); - + const result = manager.isAnyServiceAvailable(); - + expect(result).toBe(false); }); }); @@ -232,18 +236,18 @@ describe('AIServiceManager', () => { .mockReturnValueOnce('openai-key') .mockReturnValueOnce('anthropic-key') .mockReturnValueOnce(''); // No Ollama URL - + const result = manager.getAvailableProviders(); - + expect(result).toEqual(['openai', 'anthropic']); }); it('should include already created services', () => { // Mock that OpenAI has API key configured vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); - + const result = manager.getAvailableProviders(); - + expect(result).toContain('openai'); }); }); @@ -255,23 +259,23 @@ describe('AIServiceManager', () => { it('should generate completion with selected provider', async () => { vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); - + // Mock the getAvailableProviders to include openai vi.mocked(options.getOption) .mockReturnValueOnce('test-api-key') // for availability check .mockReturnValueOnce('') // for anthropic .mockReturnValueOnce('') // for ollama .mockReturnValueOnce('test-api-key'); // for service creation - + const mockResponse = { content: 'Hello response' }; const mockService = { isAvailable: vi.fn().mockReturnValue(true), generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse) }; vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any); - + const result = await manager.generateChatCompletion(messages); - + expect(result).toBe(mockResponse); expect(mockService.generateChatCompletion).toHaveBeenCalledWith(messages, {}); }); @@ -283,28 +287,28 @@ describe('AIServiceManager', () => { modelId: 'gpt-4', fullIdentifier: 'openai:gpt-4' }); - + // Mock the getAvailableProviders to include openai vi.mocked(options.getOption) .mockReturnValueOnce('test-api-key') // for availability check .mockReturnValueOnce('') // for anthropic .mockReturnValueOnce('') // for ollama .mockReturnValueOnce('test-api-key'); // for service creation - + const mockResponse = { content: 'Hello response' }; const mockService = { isAvailable: vi.fn().mockReturnValue(true), generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse) }; vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any); - - const result = await manager.generateChatCompletion(messages, { - model: 'openai:gpt-4' + + const result = await manager.generateChatCompletion(messages, { + model: 'openai:gpt-4' }); - + expect(result).toBe(mockResponse); expect(mockService.generateChatCompletion).toHaveBeenCalledWith( - messages, + messages, { model: 'gpt-4' } ); }); @@ -317,7 +321,7 @@ describe('AIServiceManager', () => { it('should throw error if no provider selected', async () => { vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null); - + await expect(manager.generateChatCompletion(messages)).rejects.toThrow( 'No AI provider is selected' ); @@ -330,13 +334,13 @@ describe('AIServiceManager', () => { modelId: 'claude-3', fullIdentifier: 'anthropic:claude-3' }); - + // Mock that openai is available vi.mocked(options.getOption) .mockReturnValueOnce('test-api-key') // for availability check .mockReturnValueOnce('') // for anthropic .mockReturnValueOnce(''); // for ollama - + await expect( manager.generateChatCompletion(messages, { model: 'anthropic:claude-3' }) ).rejects.toThrow( @@ -348,9 +352,9 @@ describe('AIServiceManager', () => { describe('getAIEnabledAsync', () => { it('should return AI enabled status', async () => { vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true); - + const result = await manager.getAIEnabledAsync(); - + expect(result).toBe(true); expect(configHelpers.isAIEnabled).toHaveBeenCalled(); }); @@ -359,9 +363,9 @@ describe('AIServiceManager', () => { describe('getAIEnabled', () => { it('should return AI enabled status synchronously', () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(true); - + const result = manager.getAIEnabled(); - + expect(result).toBe(true); expect(options.getOptionBool).toHaveBeenCalledWith('aiEnabled'); }); @@ -370,17 +374,17 @@ describe('AIServiceManager', () => { describe('initialize', () => { it('should initialize if AI is enabled', async () => { vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true); - + await manager.initialize(); - + expect(configHelpers.isAIEnabled).toHaveBeenCalled(); }); it('should not initialize if AI is disabled', async () => { vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(false); - + await manager.initialize(); - + expect(configHelpers.isAIEnabled).toHaveBeenCalled(); }); }); @@ -388,36 +392,36 @@ describe('AIServiceManager', () => { describe('getService', () => { it('should return service for specified provider', async () => { vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); - + const mockService = { isAvailable: vi.fn().mockReturnValue(true), generateChatCompletion: vi.fn() }; vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any); - + const result = await manager.getService('openai'); - + expect(result).toBe(mockService); }); it('should return selected provider service if no provider specified', async () => { vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('anthropic'); vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); - + const mockService = { isAvailable: vi.fn().mockReturnValue(true), generateChatCompletion: vi.fn() }; vi.mocked(AnthropicService).mockImplementationOnce(() => mockService as any); - + const result = await manager.getService(); - + expect(result).toBe(mockService); }); it('should throw error if specified provider not available', async () => { vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key - + await expect(manager.getService('openai')).rejects.toThrow( 'Specified provider openai is not available' ); @@ -427,17 +431,17 @@ describe('AIServiceManager', () => { describe('getSelectedProvider', () => { it('should return selected provider synchronously', () => { vi.mocked(options.getOption).mockReturnValueOnce('anthropic'); - + const result = manager.getSelectedProvider(); - + expect(result).toBe('anthropic'); }); it('should return default provider if none selected', () => { vi.mocked(options.getOption).mockReturnValueOnce(''); - + const result = manager.getSelectedProvider(); - + expect(result).toBe('openai'); }); }); @@ -446,18 +450,18 @@ describe('AIServiceManager', () => { it('should return true if provider service is available', () => { // Mock that OpenAI has API key configured vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); - + const result = manager.isProviderAvailable('openai'); - + expect(result).toBe(true); }); it('should return false if provider service not created', () => { // Mock that OpenAI has no API key configured vi.mocked(options.getOption).mockReturnValueOnce(''); - + const result = manager.isProviderAvailable('openai'); - + expect(result).toBe(false); }); }); @@ -467,13 +471,13 @@ describe('AIServiceManager', () => { // Since getProviderMetadata only returns metadata for the current active provider, // and we don't have a current provider set, it should return null const result = manager.getProviderMetadata('openai'); - + expect(result).toBeNull(); }); it('should return null for non-existing provider', () => { const result = manager.getProviderMetadata('openai'); - + expect(result).toBeNull(); }); }); @@ -485,4 +489,4 @@ describe('AIServiceManager', () => { expect(manager).toBeDefined(); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/services/llm/chat/rest_chat_service.spec.ts b/apps/server/src/services/llm/chat/rest_chat_service.spec.ts index 61edd71ca..03e52887d 100644 --- a/apps/server/src/services/llm/chat/rest_chat_service.spec.ts +++ b/apps/server/src/services/llm/chat/rest_chat_service.spec.ts @@ -15,7 +15,11 @@ vi.mock('../../log.js', () => ({ vi.mock('../../options.js', () => ({ default: { getOption: vi.fn(), - getOptionBool: vi.fn() + getOptionBool: vi.fn(), + getOptionInt: vi.fn(name => { + if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER; + return 0; + }) } })); @@ -66,14 +70,14 @@ describe('RestChatService', () => { beforeEach(async () => { vi.clearAllMocks(); - + // Get mocked modules mockOptions = (await import('../../options.js')).default; mockAiServiceManager = (await import('../ai_service_manager.js')).default; mockChatStorageService = (await import('../chat_storage_service.js')).default; - + restChatService = (await import('./rest_chat_service.js')).default; - + // Setup mock request and response mockReq = { params: {}, @@ -81,7 +85,7 @@ describe('RestChatService', () => { query: {}, method: 'POST' }; - + mockRes = { status: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), @@ -240,7 +244,7 @@ describe('RestChatService', () => { it('should handle GET request with stream parameter', async () => { mockReq.method = 'GET'; - mockReq.query = { + mockReq.query = { stream: 'true', useAdvancedContext: 'true', showThinking: 'false' @@ -419,4 +423,4 @@ describe('RestChatService', () => { expect(mockChatStorageService.getChat).toHaveBeenCalledWith('chat-123'); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/services/llm/config/configuration_helpers.spec.ts b/apps/server/src/services/llm/config/configuration_helpers.spec.ts index 82d912301..7ea5c9420 100644 --- a/apps/server/src/services/llm/config/configuration_helpers.spec.ts +++ b/apps/server/src/services/llm/config/configuration_helpers.spec.ts @@ -18,7 +18,11 @@ vi.mock('./configuration_manager.js', () => ({ vi.mock('../../options.js', () => ({ default: { getOption: vi.fn(), - getOptionBool: vi.fn() + getOptionBool: vi.fn(), + getOptionInt: vi.fn(name => { + if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER; + return 0; + }) } })); @@ -42,26 +46,26 @@ describe('configuration_helpers', () => { describe('getSelectedProvider', () => { it('should return the selected provider', async () => { vi.mocked(optionService.getOption).mockReturnValueOnce('openai'); - + const result = await configHelpers.getSelectedProvider(); - + expect(result).toBe('openai'); expect(optionService.getOption).toHaveBeenCalledWith('aiSelectedProvider'); }); it('should return null if no provider is selected', async () => { vi.mocked(optionService.getOption).mockReturnValueOnce(''); - + const result = await configHelpers.getSelectedProvider(); - + expect(result).toBeNull(); }); it('should handle invalid provider and return null', async () => { vi.mocked(optionService.getOption).mockReturnValueOnce('invalid-provider'); - + const result = await configHelpers.getSelectedProvider(); - + expect(result).toBe('invalid-provider' as ProviderType); }); }); @@ -69,7 +73,7 @@ describe('configuration_helpers', () => { describe('parseModelIdentifier', () => { it('should parse model identifier directly', () => { const result = configHelpers.parseModelIdentifier('openai:gpt-4'); - + expect(result).toStrictEqual({ provider: 'openai', modelId: 'gpt-4', @@ -79,7 +83,7 @@ describe('configuration_helpers', () => { it('should handle model without provider', () => { const result = configHelpers.parseModelIdentifier('gpt-4'); - + expect(result).toStrictEqual({ modelId: 'gpt-4', fullIdentifier: 'gpt-4' @@ -88,7 +92,7 @@ describe('configuration_helpers', () => { it('should handle empty model string', () => { const result = configHelpers.parseModelIdentifier(''); - + expect(result).toStrictEqual({ modelId: '', fullIdentifier: '' @@ -98,7 +102,7 @@ describe('configuration_helpers', () => { // Tests for special characters in model names it('should handle model names with periods', () => { const result = configHelpers.parseModelIdentifier('gpt-4.1-turbo-preview'); - + expect(result).toStrictEqual({ modelId: 'gpt-4.1-turbo-preview', fullIdentifier: 'gpt-4.1-turbo-preview' @@ -107,7 +111,7 @@ describe('configuration_helpers', () => { it('should handle model names with provider prefix and periods', () => { const result = configHelpers.parseModelIdentifier('openai:gpt-4.1-turbo'); - + expect(result).toStrictEqual({ provider: 'openai', modelId: 'gpt-4.1-turbo', @@ -117,7 +121,7 @@ describe('configuration_helpers', () => { it('should handle model names with multiple colons', () => { const result = configHelpers.parseModelIdentifier('custom:model:v1.2:latest'); - + expect(result).toStrictEqual({ modelId: 'custom:model:v1.2:latest', fullIdentifier: 'custom:model:v1.2:latest' @@ -126,7 +130,7 @@ describe('configuration_helpers', () => { it('should handle Ollama model names with colons', () => { const result = configHelpers.parseModelIdentifier('ollama:llama3.1:70b-instruct-q4_K_M'); - + expect(result).toStrictEqual({ provider: 'ollama', modelId: 'llama3.1:70b-instruct-q4_K_M', @@ -136,7 +140,7 @@ describe('configuration_helpers', () => { it('should handle model names with slashes', () => { const result = configHelpers.parseModelIdentifier('library/mistral:7b-instruct'); - + expect(result).toStrictEqual({ modelId: 'library/mistral:7b-instruct', fullIdentifier: 'library/mistral:7b-instruct' @@ -146,7 +150,7 @@ describe('configuration_helpers', () => { it('should handle complex model names with special characters', () => { const complexName = 'org/model-v1.2.3:tag@version#variant'; const result = configHelpers.parseModelIdentifier(complexName); - + expect(result).toStrictEqual({ modelId: complexName, fullIdentifier: complexName @@ -155,7 +159,7 @@ describe('configuration_helpers', () => { it('should handle model names with @ symbols', () => { const result = configHelpers.parseModelIdentifier('claude-3.5-sonnet@20241022'); - + expect(result).toStrictEqual({ modelId: 'claude-3.5-sonnet@20241022', fullIdentifier: 'claude-3.5-sonnet@20241022' @@ -165,7 +169,7 @@ describe('configuration_helpers', () => { it('should not modify or encode special characters', () => { const specialChars = 'model!@#$%^&*()_+-=[]{}|;:\'",.<>?/~`'; const result = configHelpers.parseModelIdentifier(specialChars); - + expect(result).toStrictEqual({ modelId: specialChars, fullIdentifier: specialChars @@ -176,7 +180,7 @@ describe('configuration_helpers', () => { describe('createModelConfig', () => { it('should create model config directly', () => { const result = configHelpers.createModelConfig('gpt-4', 'openai'); - + expect(result).toStrictEqual({ provider: 'openai', modelId: 'gpt-4', @@ -186,7 +190,7 @@ describe('configuration_helpers', () => { it('should handle model with provider prefix', () => { const result = configHelpers.createModelConfig('openai:gpt-4'); - + expect(result).toStrictEqual({ provider: 'openai', modelId: 'gpt-4', @@ -196,7 +200,7 @@ describe('configuration_helpers', () => { it('should fallback to openai provider when none specified', () => { const result = configHelpers.createModelConfig('gpt-4'); - + expect(result).toStrictEqual({ provider: 'openai', modelId: 'gpt-4', @@ -208,27 +212,27 @@ describe('configuration_helpers', () => { describe('getDefaultModelForProvider', () => { it('should return default model for provider', async () => { vi.mocked(optionService.getOption).mockReturnValue('gpt-4'); - + const result = await configHelpers.getDefaultModelForProvider('openai'); - + expect(result).toBe('gpt-4'); expect(optionService.getOption).toHaveBeenCalledWith('openaiDefaultModel'); }); it('should return undefined if no default model', async () => { vi.mocked(optionService.getOption).mockReturnValue(''); - + const result = await configHelpers.getDefaultModelForProvider('anthropic'); - + expect(result).toBeUndefined(); expect(optionService.getOption).toHaveBeenCalledWith('anthropicDefaultModel'); }); it('should handle ollama provider', async () => { vi.mocked(optionService.getOption).mockReturnValue('llama2'); - + const result = await configHelpers.getDefaultModelForProvider('ollama'); - + expect(result).toBe('llama2'); expect(optionService.getOption).toHaveBeenCalledWith('ollamaDefaultModel'); }); @@ -237,27 +241,27 @@ describe('configuration_helpers', () => { it('should handle OpenAI model names with periods', async () => { const modelName = 'gpt-4.1-turbo-preview'; vi.mocked(optionService.getOption).mockReturnValue(modelName); - + const result = await configHelpers.getDefaultModelForProvider('openai'); - + expect(result).toBe(modelName); }); it('should handle Anthropic model names with periods and @ symbols', async () => { const modelName = 'claude-3.5-sonnet@20241022'; vi.mocked(optionService.getOption).mockReturnValue(modelName); - + const result = await configHelpers.getDefaultModelForProvider('anthropic'); - + expect(result).toBe(modelName); }); it('should handle Ollama model names with colons and slashes', async () => { const modelName = 'library/llama3.1:70b-instruct-q4_K_M'; vi.mocked(optionService.getOption).mockReturnValue(modelName); - + const result = await configHelpers.getDefaultModelForProvider('ollama'); - + expect(result).toBe(modelName); }); }); @@ -268,9 +272,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('https://api.openai.com') // openaiBaseUrl .mockReturnValueOnce('gpt-4'); // openaiDefaultModel - + const result = await configHelpers.getProviderSettings('openai'); - + expect(result).toStrictEqual({ apiKey: 'test-key', baseUrl: 'https://api.openai.com', @@ -283,9 +287,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('anthropic-key') // anthropicApiKey .mockReturnValueOnce('https://api.anthropic.com') // anthropicBaseUrl .mockReturnValueOnce('claude-3'); // anthropicDefaultModel - + const result = await configHelpers.getProviderSettings('anthropic'); - + expect(result).toStrictEqual({ apiKey: 'anthropic-key', baseUrl: 'https://api.anthropic.com', @@ -297,9 +301,9 @@ describe('configuration_helpers', () => { vi.mocked(optionService.getOption) .mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl .mockReturnValueOnce('llama2'); // ollamaDefaultModel - + const result = await configHelpers.getProviderSettings('ollama'); - + expect(result).toStrictEqual({ baseUrl: 'http://localhost:11434', defaultModel: 'llama2' @@ -308,7 +312,7 @@ describe('configuration_helpers', () => { it('should return empty object for unknown provider', async () => { const result = await configHelpers.getProviderSettings('unknown' as ProviderType); - + expect(result).toStrictEqual({}); }); }); @@ -316,18 +320,18 @@ describe('configuration_helpers', () => { describe('isAIEnabled', () => { it('should return true if AI is enabled', async () => { vi.mocked(optionService.getOptionBool).mockReturnValue(true); - + const result = await configHelpers.isAIEnabled(); - + expect(result).toBe(true); expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled'); }); it('should return false if AI is disabled', async () => { vi.mocked(optionService.getOptionBool).mockReturnValue(false); - + const result = await configHelpers.isAIEnabled(); - + expect(result).toBe(false); expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled'); }); @@ -339,9 +343,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce(''); // openaiDefaultModel - + const result = await configHelpers.isProviderConfigured('openai'); - + expect(result).toBe(true); }); @@ -350,9 +354,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('') // openaiApiKey (empty) .mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce(''); // openaiDefaultModel - + const result = await configHelpers.isProviderConfigured('openai'); - + expect(result).toBe(false); }); @@ -361,9 +365,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('anthropic-key') // anthropicApiKey .mockReturnValueOnce('') // anthropicBaseUrl .mockReturnValueOnce(''); // anthropicDefaultModel - + const result = await configHelpers.isProviderConfigured('anthropic'); - + expect(result).toBe(true); }); @@ -371,15 +375,15 @@ describe('configuration_helpers', () => { vi.mocked(optionService.getOption) .mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl .mockReturnValueOnce(''); // ollamaDefaultModel - + const result = await configHelpers.isProviderConfigured('ollama'); - + expect(result).toBe(true); }); it('should return false for unknown provider', async () => { const result = await configHelpers.isProviderConfigured('unknown' as ProviderType); - + expect(result).toBe(false); }); }); @@ -391,17 +395,17 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce(''); // openaiDefaultModel - + const result = await configHelpers.getAvailableSelectedProvider(); - + expect(result).toBe('openai'); }); it('should return null if no provider selected', async () => { vi.mocked(optionService.getOption).mockReturnValueOnce(''); - + const result = await configHelpers.getAvailableSelectedProvider(); - + expect(result).toBeNull(); }); @@ -411,9 +415,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('') // openaiApiKey (empty) .mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce(''); // openaiDefaultModel - + const result = await configHelpers.getAvailableSelectedProvider(); - + expect(result).toBeNull(); }); }); @@ -427,9 +431,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('gpt-4'); // openaiDefaultModel - + const result = await configHelpers.validateConfiguration(); - + expect(result).toStrictEqual({ isValid: true, errors: [], @@ -439,9 +443,9 @@ describe('configuration_helpers', () => { it('should return warning when AI is disabled', async () => { vi.mocked(optionService.getOptionBool).mockReturnValue(false); - + const result = await configHelpers.validateConfiguration(); - + expect(result).toStrictEqual({ isValid: true, errors: [], @@ -452,9 +456,9 @@ describe('configuration_helpers', () => { it('should return error when no provider selected', async () => { vi.mocked(optionService.getOptionBool).mockReturnValue(true); vi.mocked(optionService.getOption).mockReturnValue(''); // no aiSelectedProvider - + const result = await configHelpers.validateConfiguration(); - + expect(result).toStrictEqual({ isValid: false, errors: ['No AI provider selected'], @@ -469,9 +473,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('') // openaiApiKey (empty) .mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce(''); // openaiDefaultModel - + const result = await configHelpers.validateConfiguration(); - + expect(result).toStrictEqual({ isValid: true, errors: [], @@ -495,9 +499,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce(''); // openaiDefaultModel - + const result = await configHelpers.getValidModelConfig('openai'); - + expect(result).toStrictEqual({ model: modelName, provider: 'openai' @@ -511,9 +515,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('anthropic-key') // anthropicApiKey .mockReturnValueOnce('') // anthropicBaseUrl .mockReturnValueOnce(''); // anthropicDefaultModel - + const result = await configHelpers.getValidModelConfig('anthropic'); - + expect(result).toStrictEqual({ model: modelName, provider: 'anthropic' @@ -526,9 +530,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce(modelName) // ollamaDefaultModel .mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl .mockReturnValueOnce(''); // ollamaDefaultModel - + const result = await configHelpers.getValidModelConfig('ollama'); - + expect(result).toStrictEqual({ model: modelName, provider: 'ollama' @@ -545,9 +549,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce(''); // openaiDefaultModel - + const result = await configHelpers.getSelectedModelConfig(); - + expect(result).toStrictEqual({ model: modelName, provider: 'openai' @@ -562,9 +566,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce(''); // openaiDefaultModel - + const result = await configHelpers.getSelectedModelConfig(); - + expect(result).toStrictEqual({ model: modelName, provider: 'openai' @@ -578,9 +582,9 @@ describe('configuration_helpers', () => { .mockReturnValueOnce(modelName) // ollamaDefaultModel .mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl .mockReturnValueOnce(''); // ollamaDefaultModel - + const result = await configHelpers.getSelectedModelConfig(); - + expect(result).toStrictEqual({ model: modelName, provider: 'ollama' @@ -595,13 +599,13 @@ describe('configuration_helpers', () => { .mockReturnValueOnce('test-key') // anthropicApiKey .mockReturnValueOnce('') // anthropicBaseUrl .mockReturnValueOnce(''); // anthropicDefaultModel - + const result = await configHelpers.getSelectedModelConfig(); - + expect(result).toStrictEqual({ model: modelName, provider: 'anthropic' }); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/services/llm/providers/anthropic_service.spec.ts b/apps/server/src/services/llm/providers/anthropic_service.spec.ts index 47266fd19..365b529f4 100644 --- a/apps/server/src/services/llm/providers/anthropic_service.spec.ts +++ b/apps/server/src/services/llm/providers/anthropic_service.spec.ts @@ -10,7 +10,11 @@ import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js'; vi.mock('../../options.js', () => ({ default: { getOption: vi.fn(), - getOptionBool: vi.fn() + getOptionBool: vi.fn(), + getOptionInt: vi.fn(name => { + if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER; + return 0; + }) } })); @@ -79,7 +83,7 @@ describe('AnthropicService', () => { beforeEach(() => { vi.clearAllMocks(); - + // Get the mocked Anthropic instance before creating the service const AnthropicMock = vi.mocked(Anthropic); mockAnthropicInstance = { @@ -122,9 +126,9 @@ describe('AnthropicService', () => { }) } }; - + AnthropicMock.mockImplementation(() => mockAnthropicInstance); - + service = new AnthropicService(); }); @@ -144,26 +148,26 @@ describe('AnthropicService', () => { it('should return true when AI is enabled and API key exists', () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); // API key - + const result = service.isAvailable(); - + expect(result).toBe(true); }); it('should return false when AI is disabled', () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled - + const result = service.isAvailable(); - + expect(result).toBe(false); }); it('should return false when no API key', () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key - + const result = service.isAvailable(); - + expect(result).toBe(false); }); }); @@ -190,9 +194,9 @@ describe('AnthropicService', () => { stream: false }; vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); - + const result = await service.generateChatCompletion(messages); - + expect(result).toEqual({ text: 'Hello! How can I help you today?', provider: 'Anthropic', @@ -214,11 +218,11 @@ describe('AnthropicService', () => { stream: false }; vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); - + const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create'); - + await service.generateChatCompletion(messages); - + const calledParams = createSpy.mock.calls[0][0] as any; expect(calledParams.messages).toEqual([ { role: 'user', content: 'Hello' } @@ -235,12 +239,12 @@ describe('AnthropicService', () => { onChunk: vi.fn() }; vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); - + const result = await service.generateChatCompletion(messages); - + // Wait for chunks to be processed await new Promise(resolve => setTimeout(resolve, 100)); - + // Check that the result exists (streaming logic is complex, so we just verify basic structure) expect(result).toBeDefined(); expect(result).toHaveProperty('text'); @@ -256,7 +260,7 @@ describe('AnthropicService', () => { properties: {} } }]; - + const mockOptions = { apiKey: 'test-key', baseUrl: 'https://api.anthropic.com', @@ -267,7 +271,7 @@ describe('AnthropicService', () => { tool_choice: { type: 'any' } }; vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); - + // Mock response with tool use mockAnthropicInstance.messages.create.mockResolvedValueOnce({ id: 'msg_123', @@ -287,9 +291,9 @@ describe('AnthropicService', () => { output_tokens: 25 } }); - + const result = await service.generateChatCompletion(messages); - + expect(result).toEqual({ text: '', provider: 'Anthropic', @@ -312,7 +316,7 @@ describe('AnthropicService', () => { it('should throw error if service not available', async () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled - + await expect(service.generateChatCompletion(messages)).rejects.toThrow( 'Anthropic service is not available' ); @@ -326,12 +330,12 @@ describe('AnthropicService', () => { stream: false }; vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); - + // Mock API error mockAnthropicInstance.messages.create.mockRejectedValueOnce( new Error('API Error: Invalid API key') ); - + await expect(service.generateChatCompletion(messages)).rejects.toThrow( 'API Error: Invalid API key' ); @@ -347,15 +351,15 @@ describe('AnthropicService', () => { stream: false }; vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); - + // Spy on Anthropic constructor const AnthropicMock = vi.mocked(Anthropic); AnthropicMock.mockClear(); - + // Create new service to trigger client creation const newService = new AnthropicService(); await newService.generateChatCompletion(messages); - + expect(AnthropicMock).toHaveBeenCalledWith({ apiKey: 'test-key', baseURL: 'https://api.anthropic.com', @@ -374,15 +378,15 @@ describe('AnthropicService', () => { stream: false }; vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); - + // Spy on Anthropic constructor const AnthropicMock = vi.mocked(Anthropic); AnthropicMock.mockClear(); - + // Create new service to trigger client creation const newService = new AnthropicService(); await newService.generateChatCompletion(messages); - + expect(AnthropicMock).toHaveBeenCalledWith({ apiKey: 'test-key', baseURL: 'https://api.anthropic.com', @@ -401,7 +405,7 @@ describe('AnthropicService', () => { stream: false }; vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); - + // Mock response with mixed content mockAnthropicInstance.messages.create.mockResolvedValueOnce({ id: 'msg_123', @@ -420,9 +424,9 @@ describe('AnthropicService', () => { output_tokens: 25 } }); - + const result = await service.generateChatCompletion(messages); - + expect(result.text).toBe('Here is the result: The calculation is complete.'); expect(result.tool_calls).toHaveLength(1); expect(result.tool_calls![0].function.name).toBe('calculate'); @@ -431,8 +435,8 @@ describe('AnthropicService', () => { it('should handle tool results in messages', async () => { const messagesWithToolResult: Message[] = [ { role: 'user', content: 'Calculate 5 + 3' }, - { - role: 'assistant', + { + role: 'assistant', content: '', tool_calls: [{ id: 'call_123', @@ -440,13 +444,13 @@ describe('AnthropicService', () => { function: { name: 'calculate', arguments: '{"x": 5, "y": 3}' } }] }, - { - role: 'tool', + { + role: 'tool', content: '8', tool_call_id: 'call_123' } ]; - + const mockOptions = { apiKey: 'test-key', baseUrl: 'https://api.anthropic.com', @@ -454,11 +458,11 @@ describe('AnthropicService', () => { stream: false }; vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); - + const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create'); - + await service.generateChatCompletion(messagesWithToolResult); - + const formattedMessages = (createSpy.mock.calls[0][0] as any).messages; expect(formattedMessages).toHaveLength(3); expect(formattedMessages[2]).toEqual({ @@ -471,4 +475,4 @@ describe('AnthropicService', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/services/llm/providers/model_selection.spec.ts b/apps/server/src/services/llm/providers/model_selection.spec.ts index c6fdfae1e..916dcd92b 100644 --- a/apps/server/src/services/llm/providers/model_selection.spec.ts +++ b/apps/server/src/services/llm/providers/model_selection.spec.ts @@ -10,7 +10,11 @@ import options from '../../options.js'; vi.mock('../../options.js', () => ({ default: { getOption: vi.fn(), - getOptionBool: vi.fn() + getOptionBool: vi.fn(), + getOptionInt: vi.fn(name => { + if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER; + return 0; + }) } })); @@ -82,7 +86,7 @@ describe('LLM Model Selection with Special Characters', () => { // Spy on getOpenAIOptions to verify model name is passed correctly const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions'); - + try { await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts); } catch (error) { @@ -108,7 +112,7 @@ describe('LLM Model Selection with Special Characters', () => { }; const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions'); - + try { await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts); } catch (error) { @@ -127,7 +131,7 @@ describe('LLM Model Selection with Special Characters', () => { }; const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions'); - + const openaiOptions = providers.getOpenAIOptions(opts); expect(openaiOptions.model).toBe(modelName); }); @@ -153,7 +157,7 @@ describe('LLM Model Selection with Special Characters', () => { }); const service = new OpenAIService(); - + // Access the private openai client through the service const client = (service as any).getClient('test-key'); const createSpy = vi.spyOn(client.chat.completions, 'create'); @@ -213,7 +217,7 @@ describe('LLM Model Selection with Special Characters', () => { }); const service = new AnthropicService(); - + // Access the private anthropic client const client = (service as any).getClient('test-key'); const createSpy = vi.spyOn(client.messages, 'create'); @@ -278,7 +282,7 @@ describe('LLM Model Selection with Special Characters', () => { const ollamaOptions = await providers.getOllamaOptions(opts); expect(ollamaOptions.model).toBe(modelName); - + // Also test with model specified in options const optsWithModel: ChatCompletionOptions = { model: 'another/model:v2.0@beta', @@ -370,7 +374,7 @@ describe('LLM Model Selection with Special Characters', () => { describe('Integration with REST API', () => { it('should pass model names correctly through REST chat service', async () => { const modelName = 'gpt-4.1-turbo-preview@latest'; - + // Mock the configuration helpers vi.doMock('../config/configuration_helpers.js', () => ({ getSelectedModelConfig: vi.fn().mockResolvedValue({ @@ -382,8 +386,8 @@ describe('LLM Model Selection with Special Characters', () => { const { getSelectedModelConfig } = await import('../config/configuration_helpers.js'); const config = await getSelectedModelConfig(); - + expect(config?.model).toBe(modelName); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/services/llm/providers/ollama_service.spec.ts b/apps/server/src/services/llm/providers/ollama_service.spec.ts index 5d03137fb..e2bee52d2 100644 --- a/apps/server/src/services/llm/providers/ollama_service.spec.ts +++ b/apps/server/src/services/llm/providers/ollama_service.spec.ts @@ -9,7 +9,11 @@ import { Ollama } from 'ollama'; vi.mock('../../options.js', () => ({ default: { getOption: vi.fn(), - getOptionBool: vi.fn() + getOptionBool: vi.fn(), + getOptionInt: vi.fn(name => { + if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER; + return 0; + }) } })); @@ -134,7 +138,7 @@ describe('OllamaService', () => { beforeEach(() => { vi.clearAllMocks(); - + // Create the mock instance before creating the service const OllamaMock = vi.mocked(Ollama); mockOllamaInstance = { @@ -191,11 +195,11 @@ describe('OllamaService', () => { ] }) }; - + OllamaMock.mockImplementation(() => mockOllamaInstance); - + service = new OllamaService(); - + // Replace the formatter with a mock after construction (service as any).formatter = { formatMessages: vi.fn().mockReturnValue([ @@ -231,26 +235,26 @@ describe('OllamaService', () => { it('should return true when AI is enabled and base URL exists', () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled vi.mocked(options.getOption).mockReturnValueOnce('http://localhost:11434'); // Base URL - + const result = service.isAvailable(); - + expect(result).toBe(true); }); it('should return false when AI is disabled', () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled - + const result = service.isAvailable(); - + expect(result).toBe(false); }); it('should return false when no base URL', () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled vi.mocked(options.getOption).mockReturnValueOnce(''); // No base URL - + const result = service.isAvailable(); - + expect(result).toBe(false); }); }); @@ -275,9 +279,9 @@ describe('OllamaService', () => { }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const result = await service.generateChatCompletion(messages); - + expect(result).toEqual({ text: 'Hello! How can I help you today?', provider: 'ollama', @@ -296,12 +300,12 @@ describe('OllamaService', () => { }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const result = await service.generateChatCompletion(messages); - + // Wait for chunks to be processed await new Promise(resolve => setTimeout(resolve, 100)); - + // For streaming, we expect a different response structure expect(result).toBeDefined(); expect(result).toHaveProperty('text'); @@ -310,7 +314,7 @@ describe('OllamaService', () => { it('should handle tools when enabled', async () => { vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const mockTools = [{ name: 'test_tool', description: 'Test tool', @@ -320,7 +324,7 @@ describe('OllamaService', () => { required: [] } }]; - + const mockOptions = { baseUrl: 'http://localhost:11434', model: 'llama2', @@ -329,18 +333,18 @@ describe('OllamaService', () => { tools: mockTools }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); - + const chatSpy = vi.spyOn(mockOllamaInstance, 'chat'); - + await service.generateChatCompletion(messages); - + const calledParams = chatSpy.mock.calls[0][0] as any; expect(calledParams.tools).toEqual(mockTools); }); it('should throw error if service not available', async () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled - + await expect(service.generateChatCompletion(messages)).rejects.toThrow( 'Ollama service is not available' ); @@ -350,14 +354,14 @@ describe('OllamaService', () => { vi.mocked(options.getOption) .mockReturnValueOnce('') // Empty base URL for ollamaBaseUrl .mockReturnValue(''); // Ensure all subsequent calls return empty - + const mockOptions = { baseUrl: '', model: 'llama2', stream: false }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); - + await expect(service.generateChatCompletion(messages)).rejects.toThrow( 'Ollama service is not available' ); @@ -365,19 +369,19 @@ describe('OllamaService', () => { it('should handle API errors', async () => { vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const mockOptions = { baseUrl: 'http://localhost:11434', model: 'llama2', stream: false }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); - + // Mock API error mockOllamaInstance.chat.mockRejectedValueOnce( new Error('Connection refused') ); - + await expect(service.generateChatCompletion(messages)).rejects.toThrow( 'Connection refused' ); @@ -385,30 +389,30 @@ describe('OllamaService', () => { it('should create client with custom fetch for debugging', async () => { vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const mockOptions = { baseUrl: 'http://localhost:11434', model: 'llama2', stream: false }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); - + // Spy on Ollama constructor const OllamaMock = vi.mocked(Ollama); OllamaMock.mockClear(); - + // Create new service to trigger client creation const newService = new OllamaService(); - + // Replace the formatter with a mock for the new service (newService as any).formatter = { formatMessages: vi.fn().mockReturnValue([ { role: 'user', content: 'Hello' } ]) }; - + await newService.generateChatCompletion(messages); - + expect(OllamaMock).toHaveBeenCalledWith({ host: 'http://localhost:11434', fetch: expect.any(Function) @@ -417,7 +421,7 @@ describe('OllamaService', () => { it('should handle tool execution feedback', async () => { vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const mockOptions = { baseUrl: 'http://localhost:11434', model: 'llama2', @@ -426,7 +430,7 @@ describe('OllamaService', () => { tools: [{ name: 'test_tool', description: 'Test', parameters: {} }] }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); - + // Mock response with tool call (arguments should be a string for Ollama) mockOllamaInstance.chat.mockResolvedValueOnce({ message: { @@ -442,9 +446,9 @@ describe('OllamaService', () => { }, done: true }); - + const result = await service.generateChatCompletion(messages); - + expect(result.tool_calls).toEqual([{ id: 'call_123', type: 'function', @@ -457,14 +461,14 @@ describe('OllamaService', () => { it('should handle mixed text and tool content', async () => { vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const mockOptions = { baseUrl: 'http://localhost:11434', model: 'llama2', stream: false }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); - + // Mock response with both text and tool calls mockOllamaInstance.chat.mockResolvedValueOnce({ message: { @@ -480,30 +484,30 @@ describe('OllamaService', () => { }, done: true }); - + const result = await service.generateChatCompletion(messages); - + expect(result.text).toBe('Let me help you with that.'); expect(result.tool_calls).toHaveLength(1); }); it('should format messages using the formatter', async () => { vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const mockOptions = { baseUrl: 'http://localhost:11434', model: 'llama2', stream: false }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); - + const formattedMessages = [{ role: 'user', content: 'Hello' }]; (service as any).formatter.formatMessages.mockReturnValueOnce(formattedMessages); - + const chatSpy = vi.spyOn(mockOllamaInstance, 'chat'); - + await service.generateChatCompletion(messages); - + expect((service as any).formatter.formatMessages).toHaveBeenCalled(); expect(chatSpy).toHaveBeenCalledWith( expect.objectContaining({ @@ -514,23 +518,23 @@ describe('OllamaService', () => { it('should handle network errors gracefully', async () => { vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const mockOptions = { baseUrl: 'http://localhost:11434', model: 'llama2', stream: false }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); - + // Mock network error global.fetch = vi.fn().mockRejectedValueOnce( new Error('Network error') ); - + mockOllamaInstance.chat.mockRejectedValueOnce( new Error('fetch failed') ); - + await expect(service.generateChatCompletion(messages)).rejects.toThrow( 'fetch failed' ); @@ -538,19 +542,19 @@ describe('OllamaService', () => { it('should validate model availability', async () => { vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const mockOptions = { baseUrl: 'http://localhost:11434', model: 'nonexistent-model', stream: false }; vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); - + // Mock model not found error mockOllamaInstance.chat.mockRejectedValueOnce( new Error('model "nonexistent-model" not found') ); - + await expect(service.generateChatCompletion(messages)).rejects.toThrow( 'model "nonexistent-model" not found' ); @@ -561,23 +565,23 @@ describe('OllamaService', () => { it('should reuse existing client', async () => { vi.mocked(options.getOptionBool).mockReturnValue(true); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); - + const mockOptions = { baseUrl: 'http://localhost:11434', model: 'llama2', stream: false }; vi.mocked(providers.getOllamaOptions).mockResolvedValue(mockOptions); - + const OllamaMock = vi.mocked(Ollama); OllamaMock.mockClear(); - + // Make two calls await service.generateChatCompletion([{ role: 'user', content: 'Hello' }]); await service.generateChatCompletion([{ role: 'user', content: 'Hi' }]); - + // Should only create client once expect(OllamaMock).toHaveBeenCalledTimes(1); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/services/llm/providers/openai_service.spec.ts b/apps/server/src/services/llm/providers/openai_service.spec.ts index 39544fabc..8adb03d84 100644 --- a/apps/server/src/services/llm/providers/openai_service.spec.ts +++ b/apps/server/src/services/llm/providers/openai_service.spec.ts @@ -2,13 +2,17 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { OpenAIService } from './openai_service.js'; import options from '../../options.js'; import * as providers from './providers.js'; -import type { ChatCompletionOptions, Message } from '../ai_interface.js'; +import type { Message } from '../ai_interface.js'; // Mock dependencies vi.mock('../../options.js', () => ({ default: { getOption: vi.fn(), - getOptionBool: vi.fn() + getOptionBool: vi.fn(), + getOptionInt: vi.fn(name => { + if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER; + return 0; + }) } })); @@ -53,17 +57,17 @@ describe('OpenAIService', () => { describe('isAvailable', () => { it('should return true when base checks pass', () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled - + const result = service.isAvailable(); - + expect(result).toBe(true); }); it('should return false when AI is disabled', () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled - + const result = service.isAvailable(); - + expect(result).toBe(false); }); }); @@ -89,7 +93,7 @@ describe('OpenAIService', () => { enableTools: false }; vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); - + // Mock the getClient method to return our mock client const mockCompletion = { id: 'chatcmpl-123', @@ -120,9 +124,9 @@ describe('OpenAIService', () => { }; vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); - + const result = await service.generateChatCompletion(messages); - + expect(result).toEqual({ text: 'Hello! How can I help you today?', model: 'gpt-3.5-turbo', @@ -144,7 +148,7 @@ describe('OpenAIService', () => { stream: true }; vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); - + // Mock the streaming response const mockStream = { [Symbol.asyncIterator]: async function* () { @@ -162,7 +166,7 @@ describe('OpenAIService', () => { }; } }; - + const mockClient = { chat: { completions: { @@ -172,9 +176,9 @@ describe('OpenAIService', () => { }; vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); - + const result = await service.generateChatCompletion(messages); - + expect(result).toHaveProperty('stream'); expect(result.text).toBe(''); expect(result.model).toBe('gpt-3.5-turbo'); @@ -183,7 +187,7 @@ describe('OpenAIService', () => { it('should throw error if service not available', async () => { vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled - + await expect(service.generateChatCompletion(messages)).rejects.toThrow( 'OpenAI service is not available' ); @@ -197,7 +201,7 @@ describe('OpenAIService', () => { stream: false }; vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); - + const mockClient = { chat: { completions: { @@ -207,7 +211,7 @@ describe('OpenAIService', () => { }; vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); - + await expect(service.generateChatCompletion(messages)).rejects.toThrow( 'API Error: Invalid API key' ); @@ -222,7 +226,7 @@ describe('OpenAIService', () => { parameters: {} } }]; - + const mockOptions = { apiKey: 'test-key', baseUrl: 'https://api.openai.com/v1', @@ -233,7 +237,7 @@ describe('OpenAIService', () => { tool_choice: 'auto' }; vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); - + const mockCompletion = { id: 'chatcmpl-123', object: 'chat.completion', @@ -263,9 +267,9 @@ describe('OpenAIService', () => { }; vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); - + await service.generateChatCompletion(messages); - + const createCall = mockClient.chat.completions.create.mock.calls[0][0]; expect(createCall.tools).toEqual(mockTools); expect(createCall.tool_choice).toBe('auto'); @@ -281,7 +285,7 @@ describe('OpenAIService', () => { tools: [{ type: 'function' as const, function: { name: 'test', description: 'test' } }] }; vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); - + const mockCompletion = { id: 'chatcmpl-123', object: 'chat.completion', @@ -319,9 +323,9 @@ describe('OpenAIService', () => { }; vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); - + const result = await service.generateChatCompletion(messages); - + expect(result).toEqual({ text: '', model: 'gpt-3.5-turbo', @@ -342,4 +346,4 @@ describe('OpenAIService', () => { }); }); }); -}); \ No newline at end of file +});