test(server): protected session timeout crashing on LLM tests

This commit is contained in:
Elian Doran 2025-10-09 12:51:11 +03:00
parent a8011e4755
commit 6b6322798e
No known key found for this signature in database
8 changed files with 323 additions and 291 deletions

View File

@ -72,7 +72,11 @@ vi.mock("../../services/options.js", () => ({
getOptionMap: vi.fn(() => new Map()), getOptionMap: vi.fn(() => new Map()),
createOption: vi.fn(), createOption: vi.fn(),
getOption: vi.fn(() => '0'), 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;
})
} }
})); }));

View File

@ -12,7 +12,11 @@ import type { AIService, ChatCompletionOptions, Message } from './ai_interface.j
vi.mock('../options.js', () => ({ vi.mock('../options.js', () => ({
default: { default: {
getOption: vi.fn(), 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', () => { describe('getSelectedProviderAsync', () => {
it('should return the selected provider', async () => { it('should return the selected provider', async () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
const result = await manager.getSelectedProviderAsync(); const result = await manager.getSelectedProviderAsync();
expect(result).toBe('openai'); expect(result).toBe('openai');
expect(configHelpers.getSelectedProvider).toHaveBeenCalled(); expect(configHelpers.getSelectedProvider).toHaveBeenCalled();
}); });
it('should return null if no provider is selected', async () => { it('should return null if no provider is selected', async () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null);
const result = await manager.getSelectedProviderAsync(); const result = await manager.getSelectedProviderAsync();
expect(result).toBeNull(); expect(result).toBeNull();
}); });
it('should handle errors and return null', async () => { it('should handle errors and return null', async () => {
vi.mocked(configHelpers.getSelectedProvider).mockRejectedValueOnce(new Error('Config error')); vi.mocked(configHelpers.getSelectedProvider).mockRejectedValueOnce(new Error('Config error'));
const result = await manager.getSelectedProviderAsync(); const result = await manager.getSelectedProviderAsync();
expect(result).toBeNull(); expect(result).toBeNull();
}); });
}); });
@ -141,9 +145,9 @@ describe('AIServiceManager', () => {
errors: [], errors: [],
warnings: [] warnings: []
}); });
const result = await manager.validateConfiguration(); const result = await manager.validateConfiguration();
expect(result).toBeNull(); expect(result).toBeNull();
}); });
@ -153,9 +157,9 @@ describe('AIServiceManager', () => {
errors: ['Missing API key', 'Invalid model'], errors: ['Missing API key', 'Invalid model'],
warnings: [] warnings: []
}); });
const result = await manager.validateConfiguration(); const result = await manager.validateConfiguration();
expect(result).toContain('There are issues with your AI configuration'); expect(result).toContain('There are issues with your AI configuration');
expect(result).toContain('Missing API key'); expect(result).toContain('Missing API key');
expect(result).toContain('Invalid model'); expect(result).toContain('Invalid model');
@ -167,9 +171,9 @@ describe('AIServiceManager', () => {
errors: [], errors: [],
warnings: ['Model not optimal'] warnings: ['Model not optimal']
}); });
const result = await manager.validateConfiguration(); const result = await manager.validateConfiguration();
expect(result).toBeNull(); expect(result).toBeNull();
}); });
}); });
@ -178,21 +182,21 @@ describe('AIServiceManager', () => {
it('should create and return the selected provider service', async () => { it('should create and return the selected provider service', async () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
const mockService = { const mockService = {
isAvailable: vi.fn().mockReturnValue(true), isAvailable: vi.fn().mockReturnValue(true),
generateChatCompletion: vi.fn() generateChatCompletion: vi.fn()
}; };
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any); vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
const result = await manager.getOrCreateAnyService(); const result = await manager.getOrCreateAnyService();
expect(result).toBe(mockService); expect(result).toBe(mockService);
}); });
it('should throw error if no provider is selected', async () => { it('should throw error if no provider is selected', async () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null);
await expect(manager.getOrCreateAnyService()).rejects.toThrow( await expect(manager.getOrCreateAnyService()).rejects.toThrow(
'No AI provider is selected' 'No AI provider is selected'
); );
@ -201,7 +205,7 @@ describe('AIServiceManager', () => {
it('should throw error if selected provider is not available', async () => { it('should throw error if selected provider is not available', async () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
await expect(manager.getOrCreateAnyService()).rejects.toThrow( await expect(manager.getOrCreateAnyService()).rejects.toThrow(
'Selected AI provider (openai) is not available' 'Selected AI provider (openai) is not available'
); );
@ -211,17 +215,17 @@ describe('AIServiceManager', () => {
describe('isAnyServiceAvailable', () => { describe('isAnyServiceAvailable', () => {
it('should return true if any provider is available', () => { it('should return true if any provider is available', () => {
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
const result = manager.isAnyServiceAvailable(); const result = manager.isAnyServiceAvailable();
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false if no providers are available', () => { it('should return false if no providers are available', () => {
vi.mocked(options.getOption).mockReturnValue(''); vi.mocked(options.getOption).mockReturnValue('');
const result = manager.isAnyServiceAvailable(); const result = manager.isAnyServiceAvailable();
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
@ -232,18 +236,18 @@ describe('AIServiceManager', () => {
.mockReturnValueOnce('openai-key') .mockReturnValueOnce('openai-key')
.mockReturnValueOnce('anthropic-key') .mockReturnValueOnce('anthropic-key')
.mockReturnValueOnce(''); // No Ollama URL .mockReturnValueOnce(''); // No Ollama URL
const result = manager.getAvailableProviders(); const result = manager.getAvailableProviders();
expect(result).toEqual(['openai', 'anthropic']); expect(result).toEqual(['openai', 'anthropic']);
}); });
it('should include already created services', () => { it('should include already created services', () => {
// Mock that OpenAI has API key configured // Mock that OpenAI has API key configured
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
const result = manager.getAvailableProviders(); const result = manager.getAvailableProviders();
expect(result).toContain('openai'); expect(result).toContain('openai');
}); });
}); });
@ -255,23 +259,23 @@ describe('AIServiceManager', () => {
it('should generate completion with selected provider', async () => { it('should generate completion with selected provider', async () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
// Mock the getAvailableProviders to include openai // Mock the getAvailableProviders to include openai
vi.mocked(options.getOption) vi.mocked(options.getOption)
.mockReturnValueOnce('test-api-key') // for availability check .mockReturnValueOnce('test-api-key') // for availability check
.mockReturnValueOnce('') // for anthropic .mockReturnValueOnce('') // for anthropic
.mockReturnValueOnce('') // for ollama .mockReturnValueOnce('') // for ollama
.mockReturnValueOnce('test-api-key'); // for service creation .mockReturnValueOnce('test-api-key'); // for service creation
const mockResponse = { content: 'Hello response' }; const mockResponse = { content: 'Hello response' };
const mockService = { const mockService = {
isAvailable: vi.fn().mockReturnValue(true), isAvailable: vi.fn().mockReturnValue(true),
generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse) generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse)
}; };
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any); vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
const result = await manager.generateChatCompletion(messages); const result = await manager.generateChatCompletion(messages);
expect(result).toBe(mockResponse); expect(result).toBe(mockResponse);
expect(mockService.generateChatCompletion).toHaveBeenCalledWith(messages, {}); expect(mockService.generateChatCompletion).toHaveBeenCalledWith(messages, {});
}); });
@ -283,28 +287,28 @@ describe('AIServiceManager', () => {
modelId: 'gpt-4', modelId: 'gpt-4',
fullIdentifier: 'openai:gpt-4' fullIdentifier: 'openai:gpt-4'
}); });
// Mock the getAvailableProviders to include openai // Mock the getAvailableProviders to include openai
vi.mocked(options.getOption) vi.mocked(options.getOption)
.mockReturnValueOnce('test-api-key') // for availability check .mockReturnValueOnce('test-api-key') // for availability check
.mockReturnValueOnce('') // for anthropic .mockReturnValueOnce('') // for anthropic
.mockReturnValueOnce('') // for ollama .mockReturnValueOnce('') // for ollama
.mockReturnValueOnce('test-api-key'); // for service creation .mockReturnValueOnce('test-api-key'); // for service creation
const mockResponse = { content: 'Hello response' }; const mockResponse = { content: 'Hello response' };
const mockService = { const mockService = {
isAvailable: vi.fn().mockReturnValue(true), isAvailable: vi.fn().mockReturnValue(true),
generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse) generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse)
}; };
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any); vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
const result = await manager.generateChatCompletion(messages, { const result = await manager.generateChatCompletion(messages, {
model: 'openai:gpt-4' model: 'openai:gpt-4'
}); });
expect(result).toBe(mockResponse); expect(result).toBe(mockResponse);
expect(mockService.generateChatCompletion).toHaveBeenCalledWith( expect(mockService.generateChatCompletion).toHaveBeenCalledWith(
messages, messages,
{ model: 'gpt-4' } { model: 'gpt-4' }
); );
}); });
@ -317,7 +321,7 @@ describe('AIServiceManager', () => {
it('should throw error if no provider selected', async () => { it('should throw error if no provider selected', async () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null);
await expect(manager.generateChatCompletion(messages)).rejects.toThrow( await expect(manager.generateChatCompletion(messages)).rejects.toThrow(
'No AI provider is selected' 'No AI provider is selected'
); );
@ -330,13 +334,13 @@ describe('AIServiceManager', () => {
modelId: 'claude-3', modelId: 'claude-3',
fullIdentifier: 'anthropic:claude-3' fullIdentifier: 'anthropic:claude-3'
}); });
// Mock that openai is available // Mock that openai is available
vi.mocked(options.getOption) vi.mocked(options.getOption)
.mockReturnValueOnce('test-api-key') // for availability check .mockReturnValueOnce('test-api-key') // for availability check
.mockReturnValueOnce('') // for anthropic .mockReturnValueOnce('') // for anthropic
.mockReturnValueOnce(''); // for ollama .mockReturnValueOnce(''); // for ollama
await expect( await expect(
manager.generateChatCompletion(messages, { model: 'anthropic:claude-3' }) manager.generateChatCompletion(messages, { model: 'anthropic:claude-3' })
).rejects.toThrow( ).rejects.toThrow(
@ -348,9 +352,9 @@ describe('AIServiceManager', () => {
describe('getAIEnabledAsync', () => { describe('getAIEnabledAsync', () => {
it('should return AI enabled status', async () => { it('should return AI enabled status', async () => {
vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true); vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true);
const result = await manager.getAIEnabledAsync(); const result = await manager.getAIEnabledAsync();
expect(result).toBe(true); expect(result).toBe(true);
expect(configHelpers.isAIEnabled).toHaveBeenCalled(); expect(configHelpers.isAIEnabled).toHaveBeenCalled();
}); });
@ -359,9 +363,9 @@ describe('AIServiceManager', () => {
describe('getAIEnabled', () => { describe('getAIEnabled', () => {
it('should return AI enabled status synchronously', () => { it('should return AI enabled status synchronously', () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); vi.mocked(options.getOptionBool).mockReturnValueOnce(true);
const result = manager.getAIEnabled(); const result = manager.getAIEnabled();
expect(result).toBe(true); expect(result).toBe(true);
expect(options.getOptionBool).toHaveBeenCalledWith('aiEnabled'); expect(options.getOptionBool).toHaveBeenCalledWith('aiEnabled');
}); });
@ -370,17 +374,17 @@ describe('AIServiceManager', () => {
describe('initialize', () => { describe('initialize', () => {
it('should initialize if AI is enabled', async () => { it('should initialize if AI is enabled', async () => {
vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true); vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true);
await manager.initialize(); await manager.initialize();
expect(configHelpers.isAIEnabled).toHaveBeenCalled(); expect(configHelpers.isAIEnabled).toHaveBeenCalled();
}); });
it('should not initialize if AI is disabled', async () => { it('should not initialize if AI is disabled', async () => {
vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(false); vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(false);
await manager.initialize(); await manager.initialize();
expect(configHelpers.isAIEnabled).toHaveBeenCalled(); expect(configHelpers.isAIEnabled).toHaveBeenCalled();
}); });
}); });
@ -388,36 +392,36 @@ describe('AIServiceManager', () => {
describe('getService', () => { describe('getService', () => {
it('should return service for specified provider', async () => { it('should return service for specified provider', async () => {
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
const mockService = { const mockService = {
isAvailable: vi.fn().mockReturnValue(true), isAvailable: vi.fn().mockReturnValue(true),
generateChatCompletion: vi.fn() generateChatCompletion: vi.fn()
}; };
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any); vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
const result = await manager.getService('openai'); const result = await manager.getService('openai');
expect(result).toBe(mockService); expect(result).toBe(mockService);
}); });
it('should return selected provider service if no provider specified', async () => { it('should return selected provider service if no provider specified', async () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('anthropic'); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('anthropic');
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
const mockService = { const mockService = {
isAvailable: vi.fn().mockReturnValue(true), isAvailable: vi.fn().mockReturnValue(true),
generateChatCompletion: vi.fn() generateChatCompletion: vi.fn()
}; };
vi.mocked(AnthropicService).mockImplementationOnce(() => mockService as any); vi.mocked(AnthropicService).mockImplementationOnce(() => mockService as any);
const result = await manager.getService(); const result = await manager.getService();
expect(result).toBe(mockService); expect(result).toBe(mockService);
}); });
it('should throw error if specified provider not available', async () => { it('should throw error if specified provider not available', async () => {
vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
await expect(manager.getService('openai')).rejects.toThrow( await expect(manager.getService('openai')).rejects.toThrow(
'Specified provider openai is not available' 'Specified provider openai is not available'
); );
@ -427,17 +431,17 @@ describe('AIServiceManager', () => {
describe('getSelectedProvider', () => { describe('getSelectedProvider', () => {
it('should return selected provider synchronously', () => { it('should return selected provider synchronously', () => {
vi.mocked(options.getOption).mockReturnValueOnce('anthropic'); vi.mocked(options.getOption).mockReturnValueOnce('anthropic');
const result = manager.getSelectedProvider(); const result = manager.getSelectedProvider();
expect(result).toBe('anthropic'); expect(result).toBe('anthropic');
}); });
it('should return default provider if none selected', () => { it('should return default provider if none selected', () => {
vi.mocked(options.getOption).mockReturnValueOnce(''); vi.mocked(options.getOption).mockReturnValueOnce('');
const result = manager.getSelectedProvider(); const result = manager.getSelectedProvider();
expect(result).toBe('openai'); expect(result).toBe('openai');
}); });
}); });
@ -446,18 +450,18 @@ describe('AIServiceManager', () => {
it('should return true if provider service is available', () => { it('should return true if provider service is available', () => {
// Mock that OpenAI has API key configured // Mock that OpenAI has API key configured
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
const result = manager.isProviderAvailable('openai'); const result = manager.isProviderAvailable('openai');
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false if provider service not created', () => { it('should return false if provider service not created', () => {
// Mock that OpenAI has no API key configured // Mock that OpenAI has no API key configured
vi.mocked(options.getOption).mockReturnValueOnce(''); vi.mocked(options.getOption).mockReturnValueOnce('');
const result = manager.isProviderAvailable('openai'); const result = manager.isProviderAvailable('openai');
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
@ -467,13 +471,13 @@ describe('AIServiceManager', () => {
// Since getProviderMetadata only returns metadata for the current active provider, // Since getProviderMetadata only returns metadata for the current active provider,
// and we don't have a current provider set, it should return null // and we don't have a current provider set, it should return null
const result = manager.getProviderMetadata('openai'); const result = manager.getProviderMetadata('openai');
expect(result).toBeNull(); expect(result).toBeNull();
}); });
it('should return null for non-existing provider', () => { it('should return null for non-existing provider', () => {
const result = manager.getProviderMetadata('openai'); const result = manager.getProviderMetadata('openai');
expect(result).toBeNull(); expect(result).toBeNull();
}); });
}); });
@ -485,4 +489,4 @@ describe('AIServiceManager', () => {
expect(manager).toBeDefined(); expect(manager).toBeDefined();
}); });
}); });
}); });

View File

@ -15,7 +15,11 @@ vi.mock('../../log.js', () => ({
vi.mock('../../options.js', () => ({ vi.mock('../../options.js', () => ({
default: { default: {
getOption: vi.fn(), 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 () => { beforeEach(async () => {
vi.clearAllMocks(); vi.clearAllMocks();
// Get mocked modules // Get mocked modules
mockOptions = (await import('../../options.js')).default; mockOptions = (await import('../../options.js')).default;
mockAiServiceManager = (await import('../ai_service_manager.js')).default; mockAiServiceManager = (await import('../ai_service_manager.js')).default;
mockChatStorageService = (await import('../chat_storage_service.js')).default; mockChatStorageService = (await import('../chat_storage_service.js')).default;
restChatService = (await import('./rest_chat_service.js')).default; restChatService = (await import('./rest_chat_service.js')).default;
// Setup mock request and response // Setup mock request and response
mockReq = { mockReq = {
params: {}, params: {},
@ -81,7 +85,7 @@ describe('RestChatService', () => {
query: {}, query: {},
method: 'POST' method: 'POST'
}; };
mockRes = { mockRes = {
status: vi.fn().mockReturnThis(), status: vi.fn().mockReturnThis(),
json: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(),
@ -240,7 +244,7 @@ describe('RestChatService', () => {
it('should handle GET request with stream parameter', async () => { it('should handle GET request with stream parameter', async () => {
mockReq.method = 'GET'; mockReq.method = 'GET';
mockReq.query = { mockReq.query = {
stream: 'true', stream: 'true',
useAdvancedContext: 'true', useAdvancedContext: 'true',
showThinking: 'false' showThinking: 'false'
@ -419,4 +423,4 @@ describe('RestChatService', () => {
expect(mockChatStorageService.getChat).toHaveBeenCalledWith('chat-123'); expect(mockChatStorageService.getChat).toHaveBeenCalledWith('chat-123');
}); });
}); });
}); });

View File

@ -18,7 +18,11 @@ vi.mock('./configuration_manager.js', () => ({
vi.mock('../../options.js', () => ({ vi.mock('../../options.js', () => ({
default: { default: {
getOption: vi.fn(), 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', () => { describe('getSelectedProvider', () => {
it('should return the selected provider', async () => { it('should return the selected provider', async () => {
vi.mocked(optionService.getOption).mockReturnValueOnce('openai'); vi.mocked(optionService.getOption).mockReturnValueOnce('openai');
const result = await configHelpers.getSelectedProvider(); const result = await configHelpers.getSelectedProvider();
expect(result).toBe('openai'); expect(result).toBe('openai');
expect(optionService.getOption).toHaveBeenCalledWith('aiSelectedProvider'); expect(optionService.getOption).toHaveBeenCalledWith('aiSelectedProvider');
}); });
it('should return null if no provider is selected', async () => { it('should return null if no provider is selected', async () => {
vi.mocked(optionService.getOption).mockReturnValueOnce(''); vi.mocked(optionService.getOption).mockReturnValueOnce('');
const result = await configHelpers.getSelectedProvider(); const result = await configHelpers.getSelectedProvider();
expect(result).toBeNull(); expect(result).toBeNull();
}); });
it('should handle invalid provider and return null', async () => { it('should handle invalid provider and return null', async () => {
vi.mocked(optionService.getOption).mockReturnValueOnce('invalid-provider'); vi.mocked(optionService.getOption).mockReturnValueOnce('invalid-provider');
const result = await configHelpers.getSelectedProvider(); const result = await configHelpers.getSelectedProvider();
expect(result).toBe('invalid-provider' as ProviderType); expect(result).toBe('invalid-provider' as ProviderType);
}); });
}); });
@ -69,7 +73,7 @@ describe('configuration_helpers', () => {
describe('parseModelIdentifier', () => { describe('parseModelIdentifier', () => {
it('should parse model identifier directly', () => { it('should parse model identifier directly', () => {
const result = configHelpers.parseModelIdentifier('openai:gpt-4'); const result = configHelpers.parseModelIdentifier('openai:gpt-4');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
provider: 'openai', provider: 'openai',
modelId: 'gpt-4', modelId: 'gpt-4',
@ -79,7 +83,7 @@ describe('configuration_helpers', () => {
it('should handle model without provider', () => { it('should handle model without provider', () => {
const result = configHelpers.parseModelIdentifier('gpt-4'); const result = configHelpers.parseModelIdentifier('gpt-4');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
modelId: 'gpt-4', modelId: 'gpt-4',
fullIdentifier: 'gpt-4' fullIdentifier: 'gpt-4'
@ -88,7 +92,7 @@ describe('configuration_helpers', () => {
it('should handle empty model string', () => { it('should handle empty model string', () => {
const result = configHelpers.parseModelIdentifier(''); const result = configHelpers.parseModelIdentifier('');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
modelId: '', modelId: '',
fullIdentifier: '' fullIdentifier: ''
@ -98,7 +102,7 @@ describe('configuration_helpers', () => {
// Tests for special characters in model names // Tests for special characters in model names
it('should handle model names with periods', () => { it('should handle model names with periods', () => {
const result = configHelpers.parseModelIdentifier('gpt-4.1-turbo-preview'); const result = configHelpers.parseModelIdentifier('gpt-4.1-turbo-preview');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
modelId: 'gpt-4.1-turbo-preview', modelId: 'gpt-4.1-turbo-preview',
fullIdentifier: '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', () => { it('should handle model names with provider prefix and periods', () => {
const result = configHelpers.parseModelIdentifier('openai:gpt-4.1-turbo'); const result = configHelpers.parseModelIdentifier('openai:gpt-4.1-turbo');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
provider: 'openai', provider: 'openai',
modelId: 'gpt-4.1-turbo', modelId: 'gpt-4.1-turbo',
@ -117,7 +121,7 @@ describe('configuration_helpers', () => {
it('should handle model names with multiple colons', () => { it('should handle model names with multiple colons', () => {
const result = configHelpers.parseModelIdentifier('custom:model:v1.2:latest'); const result = configHelpers.parseModelIdentifier('custom:model:v1.2:latest');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
modelId: 'custom:model:v1.2:latest', modelId: 'custom:model:v1.2:latest',
fullIdentifier: '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', () => { it('should handle Ollama model names with colons', () => {
const result = configHelpers.parseModelIdentifier('ollama:llama3.1:70b-instruct-q4_K_M'); const result = configHelpers.parseModelIdentifier('ollama:llama3.1:70b-instruct-q4_K_M');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
provider: 'ollama', provider: 'ollama',
modelId: 'llama3.1:70b-instruct-q4_K_M', modelId: 'llama3.1:70b-instruct-q4_K_M',
@ -136,7 +140,7 @@ describe('configuration_helpers', () => {
it('should handle model names with slashes', () => { it('should handle model names with slashes', () => {
const result = configHelpers.parseModelIdentifier('library/mistral:7b-instruct'); const result = configHelpers.parseModelIdentifier('library/mistral:7b-instruct');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
modelId: 'library/mistral:7b-instruct', modelId: 'library/mistral:7b-instruct',
fullIdentifier: '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', () => { it('should handle complex model names with special characters', () => {
const complexName = 'org/model-v1.2.3:tag@version#variant'; const complexName = 'org/model-v1.2.3:tag@version#variant';
const result = configHelpers.parseModelIdentifier(complexName); const result = configHelpers.parseModelIdentifier(complexName);
expect(result).toStrictEqual({ expect(result).toStrictEqual({
modelId: complexName, modelId: complexName,
fullIdentifier: complexName fullIdentifier: complexName
@ -155,7 +159,7 @@ describe('configuration_helpers', () => {
it('should handle model names with @ symbols', () => { it('should handle model names with @ symbols', () => {
const result = configHelpers.parseModelIdentifier('claude-3.5-sonnet@20241022'); const result = configHelpers.parseModelIdentifier('claude-3.5-sonnet@20241022');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
modelId: 'claude-3.5-sonnet@20241022', modelId: 'claude-3.5-sonnet@20241022',
fullIdentifier: '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', () => { it('should not modify or encode special characters', () => {
const specialChars = 'model!@#$%^&*()_+-=[]{}|;:\'",.<>?/~`'; const specialChars = 'model!@#$%^&*()_+-=[]{}|;:\'",.<>?/~`';
const result = configHelpers.parseModelIdentifier(specialChars); const result = configHelpers.parseModelIdentifier(specialChars);
expect(result).toStrictEqual({ expect(result).toStrictEqual({
modelId: specialChars, modelId: specialChars,
fullIdentifier: specialChars fullIdentifier: specialChars
@ -176,7 +180,7 @@ describe('configuration_helpers', () => {
describe('createModelConfig', () => { describe('createModelConfig', () => {
it('should create model config directly', () => { it('should create model config directly', () => {
const result = configHelpers.createModelConfig('gpt-4', 'openai'); const result = configHelpers.createModelConfig('gpt-4', 'openai');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
provider: 'openai', provider: 'openai',
modelId: 'gpt-4', modelId: 'gpt-4',
@ -186,7 +190,7 @@ describe('configuration_helpers', () => {
it('should handle model with provider prefix', () => { it('should handle model with provider prefix', () => {
const result = configHelpers.createModelConfig('openai:gpt-4'); const result = configHelpers.createModelConfig('openai:gpt-4');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
provider: 'openai', provider: 'openai',
modelId: 'gpt-4', modelId: 'gpt-4',
@ -196,7 +200,7 @@ describe('configuration_helpers', () => {
it('should fallback to openai provider when none specified', () => { it('should fallback to openai provider when none specified', () => {
const result = configHelpers.createModelConfig('gpt-4'); const result = configHelpers.createModelConfig('gpt-4');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
provider: 'openai', provider: 'openai',
modelId: 'gpt-4', modelId: 'gpt-4',
@ -208,27 +212,27 @@ describe('configuration_helpers', () => {
describe('getDefaultModelForProvider', () => { describe('getDefaultModelForProvider', () => {
it('should return default model for provider', async () => { it('should return default model for provider', async () => {
vi.mocked(optionService.getOption).mockReturnValue('gpt-4'); vi.mocked(optionService.getOption).mockReturnValue('gpt-4');
const result = await configHelpers.getDefaultModelForProvider('openai'); const result = await configHelpers.getDefaultModelForProvider('openai');
expect(result).toBe('gpt-4'); expect(result).toBe('gpt-4');
expect(optionService.getOption).toHaveBeenCalledWith('openaiDefaultModel'); expect(optionService.getOption).toHaveBeenCalledWith('openaiDefaultModel');
}); });
it('should return undefined if no default model', async () => { it('should return undefined if no default model', async () => {
vi.mocked(optionService.getOption).mockReturnValue(''); vi.mocked(optionService.getOption).mockReturnValue('');
const result = await configHelpers.getDefaultModelForProvider('anthropic'); const result = await configHelpers.getDefaultModelForProvider('anthropic');
expect(result).toBeUndefined(); expect(result).toBeUndefined();
expect(optionService.getOption).toHaveBeenCalledWith('anthropicDefaultModel'); expect(optionService.getOption).toHaveBeenCalledWith('anthropicDefaultModel');
}); });
it('should handle ollama provider', async () => { it('should handle ollama provider', async () => {
vi.mocked(optionService.getOption).mockReturnValue('llama2'); vi.mocked(optionService.getOption).mockReturnValue('llama2');
const result = await configHelpers.getDefaultModelForProvider('ollama'); const result = await configHelpers.getDefaultModelForProvider('ollama');
expect(result).toBe('llama2'); expect(result).toBe('llama2');
expect(optionService.getOption).toHaveBeenCalledWith('ollamaDefaultModel'); expect(optionService.getOption).toHaveBeenCalledWith('ollamaDefaultModel');
}); });
@ -237,27 +241,27 @@ describe('configuration_helpers', () => {
it('should handle OpenAI model names with periods', async () => { it('should handle OpenAI model names with periods', async () => {
const modelName = 'gpt-4.1-turbo-preview'; const modelName = 'gpt-4.1-turbo-preview';
vi.mocked(optionService.getOption).mockReturnValue(modelName); vi.mocked(optionService.getOption).mockReturnValue(modelName);
const result = await configHelpers.getDefaultModelForProvider('openai'); const result = await configHelpers.getDefaultModelForProvider('openai');
expect(result).toBe(modelName); expect(result).toBe(modelName);
}); });
it('should handle Anthropic model names with periods and @ symbols', async () => { it('should handle Anthropic model names with periods and @ symbols', async () => {
const modelName = 'claude-3.5-sonnet@20241022'; const modelName = 'claude-3.5-sonnet@20241022';
vi.mocked(optionService.getOption).mockReturnValue(modelName); vi.mocked(optionService.getOption).mockReturnValue(modelName);
const result = await configHelpers.getDefaultModelForProvider('anthropic'); const result = await configHelpers.getDefaultModelForProvider('anthropic');
expect(result).toBe(modelName); expect(result).toBe(modelName);
}); });
it('should handle Ollama model names with colons and slashes', async () => { it('should handle Ollama model names with colons and slashes', async () => {
const modelName = 'library/llama3.1:70b-instruct-q4_K_M'; const modelName = 'library/llama3.1:70b-instruct-q4_K_M';
vi.mocked(optionService.getOption).mockReturnValue(modelName); vi.mocked(optionService.getOption).mockReturnValue(modelName);
const result = await configHelpers.getDefaultModelForProvider('ollama'); const result = await configHelpers.getDefaultModelForProvider('ollama');
expect(result).toBe(modelName); expect(result).toBe(modelName);
}); });
}); });
@ -268,9 +272,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('test-key') // openaiApiKey
.mockReturnValueOnce('https://api.openai.com') // openaiBaseUrl .mockReturnValueOnce('https://api.openai.com') // openaiBaseUrl
.mockReturnValueOnce('gpt-4'); // openaiDefaultModel .mockReturnValueOnce('gpt-4'); // openaiDefaultModel
const result = await configHelpers.getProviderSettings('openai'); const result = await configHelpers.getProviderSettings('openai');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
apiKey: 'test-key', apiKey: 'test-key',
baseUrl: 'https://api.openai.com', baseUrl: 'https://api.openai.com',
@ -283,9 +287,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('anthropic-key') // anthropicApiKey .mockReturnValueOnce('anthropic-key') // anthropicApiKey
.mockReturnValueOnce('https://api.anthropic.com') // anthropicBaseUrl .mockReturnValueOnce('https://api.anthropic.com') // anthropicBaseUrl
.mockReturnValueOnce('claude-3'); // anthropicDefaultModel .mockReturnValueOnce('claude-3'); // anthropicDefaultModel
const result = await configHelpers.getProviderSettings('anthropic'); const result = await configHelpers.getProviderSettings('anthropic');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
apiKey: 'anthropic-key', apiKey: 'anthropic-key',
baseUrl: 'https://api.anthropic.com', baseUrl: 'https://api.anthropic.com',
@ -297,9 +301,9 @@ describe('configuration_helpers', () => {
vi.mocked(optionService.getOption) vi.mocked(optionService.getOption)
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl .mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
.mockReturnValueOnce('llama2'); // ollamaDefaultModel .mockReturnValueOnce('llama2'); // ollamaDefaultModel
const result = await configHelpers.getProviderSettings('ollama'); const result = await configHelpers.getProviderSettings('ollama');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
defaultModel: 'llama2' defaultModel: 'llama2'
@ -308,7 +312,7 @@ describe('configuration_helpers', () => {
it('should return empty object for unknown provider', async () => { it('should return empty object for unknown provider', async () => {
const result = await configHelpers.getProviderSettings('unknown' as ProviderType); const result = await configHelpers.getProviderSettings('unknown' as ProviderType);
expect(result).toStrictEqual({}); expect(result).toStrictEqual({});
}); });
}); });
@ -316,18 +320,18 @@ describe('configuration_helpers', () => {
describe('isAIEnabled', () => { describe('isAIEnabled', () => {
it('should return true if AI is enabled', async () => { it('should return true if AI is enabled', async () => {
vi.mocked(optionService.getOptionBool).mockReturnValue(true); vi.mocked(optionService.getOptionBool).mockReturnValue(true);
const result = await configHelpers.isAIEnabled(); const result = await configHelpers.isAIEnabled();
expect(result).toBe(true); expect(result).toBe(true);
expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled'); expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled');
}); });
it('should return false if AI is disabled', async () => { it('should return false if AI is disabled', async () => {
vi.mocked(optionService.getOptionBool).mockReturnValue(false); vi.mocked(optionService.getOptionBool).mockReturnValue(false);
const result = await configHelpers.isAIEnabled(); const result = await configHelpers.isAIEnabled();
expect(result).toBe(false); expect(result).toBe(false);
expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled'); expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled');
}); });
@ -339,9 +343,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('test-key') // openaiApiKey
.mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('') // openaiBaseUrl
.mockReturnValueOnce(''); // openaiDefaultModel .mockReturnValueOnce(''); // openaiDefaultModel
const result = await configHelpers.isProviderConfigured('openai'); const result = await configHelpers.isProviderConfigured('openai');
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -350,9 +354,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('') // openaiApiKey (empty) .mockReturnValueOnce('') // openaiApiKey (empty)
.mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('') // openaiBaseUrl
.mockReturnValueOnce(''); // openaiDefaultModel .mockReturnValueOnce(''); // openaiDefaultModel
const result = await configHelpers.isProviderConfigured('openai'); const result = await configHelpers.isProviderConfigured('openai');
expect(result).toBe(false); expect(result).toBe(false);
}); });
@ -361,9 +365,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('anthropic-key') // anthropicApiKey .mockReturnValueOnce('anthropic-key') // anthropicApiKey
.mockReturnValueOnce('') // anthropicBaseUrl .mockReturnValueOnce('') // anthropicBaseUrl
.mockReturnValueOnce(''); // anthropicDefaultModel .mockReturnValueOnce(''); // anthropicDefaultModel
const result = await configHelpers.isProviderConfigured('anthropic'); const result = await configHelpers.isProviderConfigured('anthropic');
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -371,15 +375,15 @@ describe('configuration_helpers', () => {
vi.mocked(optionService.getOption) vi.mocked(optionService.getOption)
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl .mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
.mockReturnValueOnce(''); // ollamaDefaultModel .mockReturnValueOnce(''); // ollamaDefaultModel
const result = await configHelpers.isProviderConfigured('ollama'); const result = await configHelpers.isProviderConfigured('ollama');
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false for unknown provider', async () => { it('should return false for unknown provider', async () => {
const result = await configHelpers.isProviderConfigured('unknown' as ProviderType); const result = await configHelpers.isProviderConfigured('unknown' as ProviderType);
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
@ -391,17 +395,17 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('test-key') // openaiApiKey
.mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('') // openaiBaseUrl
.mockReturnValueOnce(''); // openaiDefaultModel .mockReturnValueOnce(''); // openaiDefaultModel
const result = await configHelpers.getAvailableSelectedProvider(); const result = await configHelpers.getAvailableSelectedProvider();
expect(result).toBe('openai'); expect(result).toBe('openai');
}); });
it('should return null if no provider selected', async () => { it('should return null if no provider selected', async () => {
vi.mocked(optionService.getOption).mockReturnValueOnce(''); vi.mocked(optionService.getOption).mockReturnValueOnce('');
const result = await configHelpers.getAvailableSelectedProvider(); const result = await configHelpers.getAvailableSelectedProvider();
expect(result).toBeNull(); expect(result).toBeNull();
}); });
@ -411,9 +415,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('') // openaiApiKey (empty) .mockReturnValueOnce('') // openaiApiKey (empty)
.mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('') // openaiBaseUrl
.mockReturnValueOnce(''); // openaiDefaultModel .mockReturnValueOnce(''); // openaiDefaultModel
const result = await configHelpers.getAvailableSelectedProvider(); const result = await configHelpers.getAvailableSelectedProvider();
expect(result).toBeNull(); expect(result).toBeNull();
}); });
}); });
@ -427,9 +431,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('test-key') // openaiApiKey
.mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('') // openaiBaseUrl
.mockReturnValueOnce('gpt-4'); // openaiDefaultModel .mockReturnValueOnce('gpt-4'); // openaiDefaultModel
const result = await configHelpers.validateConfiguration(); const result = await configHelpers.validateConfiguration();
expect(result).toStrictEqual({ expect(result).toStrictEqual({
isValid: true, isValid: true,
errors: [], errors: [],
@ -439,9 +443,9 @@ describe('configuration_helpers', () => {
it('should return warning when AI is disabled', async () => { it('should return warning when AI is disabled', async () => {
vi.mocked(optionService.getOptionBool).mockReturnValue(false); vi.mocked(optionService.getOptionBool).mockReturnValue(false);
const result = await configHelpers.validateConfiguration(); const result = await configHelpers.validateConfiguration();
expect(result).toStrictEqual({ expect(result).toStrictEqual({
isValid: true, isValid: true,
errors: [], errors: [],
@ -452,9 +456,9 @@ describe('configuration_helpers', () => {
it('should return error when no provider selected', async () => { it('should return error when no provider selected', async () => {
vi.mocked(optionService.getOptionBool).mockReturnValue(true); vi.mocked(optionService.getOptionBool).mockReturnValue(true);
vi.mocked(optionService.getOption).mockReturnValue(''); // no aiSelectedProvider vi.mocked(optionService.getOption).mockReturnValue(''); // no aiSelectedProvider
const result = await configHelpers.validateConfiguration(); const result = await configHelpers.validateConfiguration();
expect(result).toStrictEqual({ expect(result).toStrictEqual({
isValid: false, isValid: false,
errors: ['No AI provider selected'], errors: ['No AI provider selected'],
@ -469,9 +473,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('') // openaiApiKey (empty) .mockReturnValueOnce('') // openaiApiKey (empty)
.mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('') // openaiBaseUrl
.mockReturnValueOnce(''); // openaiDefaultModel .mockReturnValueOnce(''); // openaiDefaultModel
const result = await configHelpers.validateConfiguration(); const result = await configHelpers.validateConfiguration();
expect(result).toStrictEqual({ expect(result).toStrictEqual({
isValid: true, isValid: true,
errors: [], errors: [],
@ -495,9 +499,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('test-key') // openaiApiKey
.mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('') // openaiBaseUrl
.mockReturnValueOnce(''); // openaiDefaultModel .mockReturnValueOnce(''); // openaiDefaultModel
const result = await configHelpers.getValidModelConfig('openai'); const result = await configHelpers.getValidModelConfig('openai');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
model: modelName, model: modelName,
provider: 'openai' provider: 'openai'
@ -511,9 +515,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('anthropic-key') // anthropicApiKey .mockReturnValueOnce('anthropic-key') // anthropicApiKey
.mockReturnValueOnce('') // anthropicBaseUrl .mockReturnValueOnce('') // anthropicBaseUrl
.mockReturnValueOnce(''); // anthropicDefaultModel .mockReturnValueOnce(''); // anthropicDefaultModel
const result = await configHelpers.getValidModelConfig('anthropic'); const result = await configHelpers.getValidModelConfig('anthropic');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
model: modelName, model: modelName,
provider: 'anthropic' provider: 'anthropic'
@ -526,9 +530,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce(modelName) // ollamaDefaultModel .mockReturnValueOnce(modelName) // ollamaDefaultModel
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl .mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
.mockReturnValueOnce(''); // ollamaDefaultModel .mockReturnValueOnce(''); // ollamaDefaultModel
const result = await configHelpers.getValidModelConfig('ollama'); const result = await configHelpers.getValidModelConfig('ollama');
expect(result).toStrictEqual({ expect(result).toStrictEqual({
model: modelName, model: modelName,
provider: 'ollama' provider: 'ollama'
@ -545,9 +549,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('test-key') // openaiApiKey
.mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('') // openaiBaseUrl
.mockReturnValueOnce(''); // openaiDefaultModel .mockReturnValueOnce(''); // openaiDefaultModel
const result = await configHelpers.getSelectedModelConfig(); const result = await configHelpers.getSelectedModelConfig();
expect(result).toStrictEqual({ expect(result).toStrictEqual({
model: modelName, model: modelName,
provider: 'openai' provider: 'openai'
@ -562,9 +566,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('test-key') // openaiApiKey .mockReturnValueOnce('test-key') // openaiApiKey
.mockReturnValueOnce('') // openaiBaseUrl .mockReturnValueOnce('') // openaiBaseUrl
.mockReturnValueOnce(''); // openaiDefaultModel .mockReturnValueOnce(''); // openaiDefaultModel
const result = await configHelpers.getSelectedModelConfig(); const result = await configHelpers.getSelectedModelConfig();
expect(result).toStrictEqual({ expect(result).toStrictEqual({
model: modelName, model: modelName,
provider: 'openai' provider: 'openai'
@ -578,9 +582,9 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce(modelName) // ollamaDefaultModel .mockReturnValueOnce(modelName) // ollamaDefaultModel
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl .mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
.mockReturnValueOnce(''); // ollamaDefaultModel .mockReturnValueOnce(''); // ollamaDefaultModel
const result = await configHelpers.getSelectedModelConfig(); const result = await configHelpers.getSelectedModelConfig();
expect(result).toStrictEqual({ expect(result).toStrictEqual({
model: modelName, model: modelName,
provider: 'ollama' provider: 'ollama'
@ -595,13 +599,13 @@ describe('configuration_helpers', () => {
.mockReturnValueOnce('test-key') // anthropicApiKey .mockReturnValueOnce('test-key') // anthropicApiKey
.mockReturnValueOnce('') // anthropicBaseUrl .mockReturnValueOnce('') // anthropicBaseUrl
.mockReturnValueOnce(''); // anthropicDefaultModel .mockReturnValueOnce(''); // anthropicDefaultModel
const result = await configHelpers.getSelectedModelConfig(); const result = await configHelpers.getSelectedModelConfig();
expect(result).toStrictEqual({ expect(result).toStrictEqual({
model: modelName, model: modelName,
provider: 'anthropic' provider: 'anthropic'
}); });
}); });
}); });
}); });

View File

@ -10,7 +10,11 @@ import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js';
vi.mock('../../options.js', () => ({ vi.mock('../../options.js', () => ({
default: { default: {
getOption: vi.fn(), 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(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
// Get the mocked Anthropic instance before creating the service // Get the mocked Anthropic instance before creating the service
const AnthropicMock = vi.mocked(Anthropic); const AnthropicMock = vi.mocked(Anthropic);
mockAnthropicInstance = { mockAnthropicInstance = {
@ -122,9 +126,9 @@ describe('AnthropicService', () => {
}) })
} }
}; };
AnthropicMock.mockImplementation(() => mockAnthropicInstance); AnthropicMock.mockImplementation(() => mockAnthropicInstance);
service = new AnthropicService(); service = new AnthropicService();
}); });
@ -144,26 +148,26 @@ describe('AnthropicService', () => {
it('should return true when AI is enabled and API key exists', () => { it('should return true when AI is enabled and API key exists', () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); // API key vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); // API key
const result = service.isAvailable(); const result = service.isAvailable();
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false when AI is disabled', () => { it('should return false when AI is disabled', () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
const result = service.isAvailable(); const result = service.isAvailable();
expect(result).toBe(false); expect(result).toBe(false);
}); });
it('should return false when no API key', () => { it('should return false when no API key', () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
const result = service.isAvailable(); const result = service.isAvailable();
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
@ -190,9 +194,9 @@ describe('AnthropicService', () => {
stream: false stream: false
}; };
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result).toEqual({ expect(result).toEqual({
text: 'Hello! How can I help you today?', text: 'Hello! How can I help you today?',
provider: 'Anthropic', provider: 'Anthropic',
@ -214,11 +218,11 @@ describe('AnthropicService', () => {
stream: false stream: false
}; };
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create'); const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create');
await service.generateChatCompletion(messages); await service.generateChatCompletion(messages);
const calledParams = createSpy.mock.calls[0][0] as any; const calledParams = createSpy.mock.calls[0][0] as any;
expect(calledParams.messages).toEqual([ expect(calledParams.messages).toEqual([
{ role: 'user', content: 'Hello' } { role: 'user', content: 'Hello' }
@ -235,12 +239,12 @@ describe('AnthropicService', () => {
onChunk: vi.fn() onChunk: vi.fn()
}; };
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
// Wait for chunks to be processed // Wait for chunks to be processed
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
// Check that the result exists (streaming logic is complex, so we just verify basic structure) // Check that the result exists (streaming logic is complex, so we just verify basic structure)
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result).toHaveProperty('text'); expect(result).toHaveProperty('text');
@ -256,7 +260,7 @@ describe('AnthropicService', () => {
properties: {} properties: {}
} }
}]; }];
const mockOptions = { const mockOptions = {
apiKey: 'test-key', apiKey: 'test-key',
baseUrl: 'https://api.anthropic.com', baseUrl: 'https://api.anthropic.com',
@ -267,7 +271,7 @@ describe('AnthropicService', () => {
tool_choice: { type: 'any' } tool_choice: { type: 'any' }
}; };
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
// Mock response with tool use // Mock response with tool use
mockAnthropicInstance.messages.create.mockResolvedValueOnce({ mockAnthropicInstance.messages.create.mockResolvedValueOnce({
id: 'msg_123', id: 'msg_123',
@ -287,9 +291,9 @@ describe('AnthropicService', () => {
output_tokens: 25 output_tokens: 25
} }
}); });
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result).toEqual({ expect(result).toEqual({
text: '', text: '',
provider: 'Anthropic', provider: 'Anthropic',
@ -312,7 +316,7 @@ describe('AnthropicService', () => {
it('should throw error if service not available', async () => { it('should throw error if service not available', async () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'Anthropic service is not available' 'Anthropic service is not available'
); );
@ -326,12 +330,12 @@ describe('AnthropicService', () => {
stream: false stream: false
}; };
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
// Mock API error // Mock API error
mockAnthropicInstance.messages.create.mockRejectedValueOnce( mockAnthropicInstance.messages.create.mockRejectedValueOnce(
new Error('API Error: Invalid API key') new Error('API Error: Invalid API key')
); );
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'API Error: Invalid API key' 'API Error: Invalid API key'
); );
@ -347,15 +351,15 @@ describe('AnthropicService', () => {
stream: false stream: false
}; };
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
// Spy on Anthropic constructor // Spy on Anthropic constructor
const AnthropicMock = vi.mocked(Anthropic); const AnthropicMock = vi.mocked(Anthropic);
AnthropicMock.mockClear(); AnthropicMock.mockClear();
// Create new service to trigger client creation // Create new service to trigger client creation
const newService = new AnthropicService(); const newService = new AnthropicService();
await newService.generateChatCompletion(messages); await newService.generateChatCompletion(messages);
expect(AnthropicMock).toHaveBeenCalledWith({ expect(AnthropicMock).toHaveBeenCalledWith({
apiKey: 'test-key', apiKey: 'test-key',
baseURL: 'https://api.anthropic.com', baseURL: 'https://api.anthropic.com',
@ -374,15 +378,15 @@ describe('AnthropicService', () => {
stream: false stream: false
}; };
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
// Spy on Anthropic constructor // Spy on Anthropic constructor
const AnthropicMock = vi.mocked(Anthropic); const AnthropicMock = vi.mocked(Anthropic);
AnthropicMock.mockClear(); AnthropicMock.mockClear();
// Create new service to trigger client creation // Create new service to trigger client creation
const newService = new AnthropicService(); const newService = new AnthropicService();
await newService.generateChatCompletion(messages); await newService.generateChatCompletion(messages);
expect(AnthropicMock).toHaveBeenCalledWith({ expect(AnthropicMock).toHaveBeenCalledWith({
apiKey: 'test-key', apiKey: 'test-key',
baseURL: 'https://api.anthropic.com', baseURL: 'https://api.anthropic.com',
@ -401,7 +405,7 @@ describe('AnthropicService', () => {
stream: false stream: false
}; };
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
// Mock response with mixed content // Mock response with mixed content
mockAnthropicInstance.messages.create.mockResolvedValueOnce({ mockAnthropicInstance.messages.create.mockResolvedValueOnce({
id: 'msg_123', id: 'msg_123',
@ -420,9 +424,9 @@ describe('AnthropicService', () => {
output_tokens: 25 output_tokens: 25
} }
}); });
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result.text).toBe('Here is the result: The calculation is complete.'); expect(result.text).toBe('Here is the result: The calculation is complete.');
expect(result.tool_calls).toHaveLength(1); expect(result.tool_calls).toHaveLength(1);
expect(result.tool_calls![0].function.name).toBe('calculate'); expect(result.tool_calls![0].function.name).toBe('calculate');
@ -431,8 +435,8 @@ describe('AnthropicService', () => {
it('should handle tool results in messages', async () => { it('should handle tool results in messages', async () => {
const messagesWithToolResult: Message[] = [ const messagesWithToolResult: Message[] = [
{ role: 'user', content: 'Calculate 5 + 3' }, { role: 'user', content: 'Calculate 5 + 3' },
{ {
role: 'assistant', role: 'assistant',
content: '', content: '',
tool_calls: [{ tool_calls: [{
id: 'call_123', id: 'call_123',
@ -440,13 +444,13 @@ describe('AnthropicService', () => {
function: { name: 'calculate', arguments: '{"x": 5, "y": 3}' } function: { name: 'calculate', arguments: '{"x": 5, "y": 3}' }
}] }]
}, },
{ {
role: 'tool', role: 'tool',
content: '8', content: '8',
tool_call_id: 'call_123' tool_call_id: 'call_123'
} }
]; ];
const mockOptions = { const mockOptions = {
apiKey: 'test-key', apiKey: 'test-key',
baseUrl: 'https://api.anthropic.com', baseUrl: 'https://api.anthropic.com',
@ -454,11 +458,11 @@ describe('AnthropicService', () => {
stream: false stream: false
}; };
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create'); const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create');
await service.generateChatCompletion(messagesWithToolResult); await service.generateChatCompletion(messagesWithToolResult);
const formattedMessages = (createSpy.mock.calls[0][0] as any).messages; const formattedMessages = (createSpy.mock.calls[0][0] as any).messages;
expect(formattedMessages).toHaveLength(3); expect(formattedMessages).toHaveLength(3);
expect(formattedMessages[2]).toEqual({ expect(formattedMessages[2]).toEqual({
@ -471,4 +475,4 @@ describe('AnthropicService', () => {
}); });
}); });
}); });
}); });

View File

@ -10,7 +10,11 @@ import options from '../../options.js';
vi.mock('../../options.js', () => ({ vi.mock('../../options.js', () => ({
default: { default: {
getOption: vi.fn(), 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 // Spy on getOpenAIOptions to verify model name is passed correctly
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions'); const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
try { try {
await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts); await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts);
} catch (error) { } catch (error) {
@ -108,7 +112,7 @@ describe('LLM Model Selection with Special Characters', () => {
}; };
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions'); const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
try { try {
await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts); await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts);
} catch (error) { } catch (error) {
@ -127,7 +131,7 @@ describe('LLM Model Selection with Special Characters', () => {
}; };
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions'); const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
const openaiOptions = providers.getOpenAIOptions(opts); const openaiOptions = providers.getOpenAIOptions(opts);
expect(openaiOptions.model).toBe(modelName); expect(openaiOptions.model).toBe(modelName);
}); });
@ -153,7 +157,7 @@ describe('LLM Model Selection with Special Characters', () => {
}); });
const service = new OpenAIService(); const service = new OpenAIService();
// Access the private openai client through the service // Access the private openai client through the service
const client = (service as any).getClient('test-key'); const client = (service as any).getClient('test-key');
const createSpy = vi.spyOn(client.chat.completions, 'create'); const createSpy = vi.spyOn(client.chat.completions, 'create');
@ -213,7 +217,7 @@ describe('LLM Model Selection with Special Characters', () => {
}); });
const service = new AnthropicService(); const service = new AnthropicService();
// Access the private anthropic client // Access the private anthropic client
const client = (service as any).getClient('test-key'); const client = (service as any).getClient('test-key');
const createSpy = vi.spyOn(client.messages, 'create'); const createSpy = vi.spyOn(client.messages, 'create');
@ -278,7 +282,7 @@ describe('LLM Model Selection with Special Characters', () => {
const ollamaOptions = await providers.getOllamaOptions(opts); const ollamaOptions = await providers.getOllamaOptions(opts);
expect(ollamaOptions.model).toBe(modelName); expect(ollamaOptions.model).toBe(modelName);
// Also test with model specified in options // Also test with model specified in options
const optsWithModel: ChatCompletionOptions = { const optsWithModel: ChatCompletionOptions = {
model: 'another/model:v2.0@beta', model: 'another/model:v2.0@beta',
@ -370,7 +374,7 @@ describe('LLM Model Selection with Special Characters', () => {
describe('Integration with REST API', () => { describe('Integration with REST API', () => {
it('should pass model names correctly through REST chat service', async () => { it('should pass model names correctly through REST chat service', async () => {
const modelName = 'gpt-4.1-turbo-preview@latest'; const modelName = 'gpt-4.1-turbo-preview@latest';
// Mock the configuration helpers // Mock the configuration helpers
vi.doMock('../config/configuration_helpers.js', () => ({ vi.doMock('../config/configuration_helpers.js', () => ({
getSelectedModelConfig: vi.fn().mockResolvedValue({ getSelectedModelConfig: vi.fn().mockResolvedValue({
@ -382,8 +386,8 @@ describe('LLM Model Selection with Special Characters', () => {
const { getSelectedModelConfig } = await import('../config/configuration_helpers.js'); const { getSelectedModelConfig } = await import('../config/configuration_helpers.js');
const config = await getSelectedModelConfig(); const config = await getSelectedModelConfig();
expect(config?.model).toBe(modelName); expect(config?.model).toBe(modelName);
}); });
}); });
}); });

View File

@ -9,7 +9,11 @@ import { Ollama } from 'ollama';
vi.mock('../../options.js', () => ({ vi.mock('../../options.js', () => ({
default: { default: {
getOption: vi.fn(), 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(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
// Create the mock instance before creating the service // Create the mock instance before creating the service
const OllamaMock = vi.mocked(Ollama); const OllamaMock = vi.mocked(Ollama);
mockOllamaInstance = { mockOllamaInstance = {
@ -191,11 +195,11 @@ describe('OllamaService', () => {
] ]
}) })
}; };
OllamaMock.mockImplementation(() => mockOllamaInstance); OllamaMock.mockImplementation(() => mockOllamaInstance);
service = new OllamaService(); service = new OllamaService();
// Replace the formatter with a mock after construction // Replace the formatter with a mock after construction
(service as any).formatter = { (service as any).formatter = {
formatMessages: vi.fn().mockReturnValue([ formatMessages: vi.fn().mockReturnValue([
@ -231,26 +235,26 @@ describe('OllamaService', () => {
it('should return true when AI is enabled and base URL exists', () => { it('should return true when AI is enabled and base URL exists', () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
vi.mocked(options.getOption).mockReturnValueOnce('http://localhost:11434'); // Base URL vi.mocked(options.getOption).mockReturnValueOnce('http://localhost:11434'); // Base URL
const result = service.isAvailable(); const result = service.isAvailable();
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false when AI is disabled', () => { it('should return false when AI is disabled', () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
const result = service.isAvailable(); const result = service.isAvailable();
expect(result).toBe(false); expect(result).toBe(false);
}); });
it('should return false when no base URL', () => { it('should return false when no base URL', () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
vi.mocked(options.getOption).mockReturnValueOnce(''); // No base URL vi.mocked(options.getOption).mockReturnValueOnce(''); // No base URL
const result = service.isAvailable(); const result = service.isAvailable();
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
@ -275,9 +279,9 @@ describe('OllamaService', () => {
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result).toEqual({ expect(result).toEqual({
text: 'Hello! How can I help you today?', text: 'Hello! How can I help you today?',
provider: 'ollama', provider: 'ollama',
@ -296,12 +300,12 @@ describe('OllamaService', () => {
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
// Wait for chunks to be processed // Wait for chunks to be processed
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
// For streaming, we expect a different response structure // For streaming, we expect a different response structure
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result).toHaveProperty('text'); expect(result).toHaveProperty('text');
@ -310,7 +314,7 @@ describe('OllamaService', () => {
it('should handle tools when enabled', async () => { it('should handle tools when enabled', async () => {
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const mockTools = [{ const mockTools = [{
name: 'test_tool', name: 'test_tool',
description: 'Test tool', description: 'Test tool',
@ -320,7 +324,7 @@ describe('OllamaService', () => {
required: [] required: []
} }
}]; }];
const mockOptions = { const mockOptions = {
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
model: 'llama2', model: 'llama2',
@ -329,18 +333,18 @@ describe('OllamaService', () => {
tools: mockTools tools: mockTools
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
const chatSpy = vi.spyOn(mockOllamaInstance, 'chat'); const chatSpy = vi.spyOn(mockOllamaInstance, 'chat');
await service.generateChatCompletion(messages); await service.generateChatCompletion(messages);
const calledParams = chatSpy.mock.calls[0][0] as any; const calledParams = chatSpy.mock.calls[0][0] as any;
expect(calledParams.tools).toEqual(mockTools); expect(calledParams.tools).toEqual(mockTools);
}); });
it('should throw error if service not available', async () => { it('should throw error if service not available', async () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'Ollama service is not available' 'Ollama service is not available'
); );
@ -350,14 +354,14 @@ describe('OllamaService', () => {
vi.mocked(options.getOption) vi.mocked(options.getOption)
.mockReturnValueOnce('') // Empty base URL for ollamaBaseUrl .mockReturnValueOnce('') // Empty base URL for ollamaBaseUrl
.mockReturnValue(''); // Ensure all subsequent calls return empty .mockReturnValue(''); // Ensure all subsequent calls return empty
const mockOptions = { const mockOptions = {
baseUrl: '', baseUrl: '',
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'Ollama service is not available' 'Ollama service is not available'
); );
@ -365,19 +369,19 @@ describe('OllamaService', () => {
it('should handle API errors', async () => { it('should handle API errors', async () => {
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const mockOptions = { const mockOptions = {
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Mock API error // Mock API error
mockOllamaInstance.chat.mockRejectedValueOnce( mockOllamaInstance.chat.mockRejectedValueOnce(
new Error('Connection refused') new Error('Connection refused')
); );
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'Connection refused' 'Connection refused'
); );
@ -385,30 +389,30 @@ describe('OllamaService', () => {
it('should create client with custom fetch for debugging', async () => { it('should create client with custom fetch for debugging', async () => {
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const mockOptions = { const mockOptions = {
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Spy on Ollama constructor // Spy on Ollama constructor
const OllamaMock = vi.mocked(Ollama); const OllamaMock = vi.mocked(Ollama);
OllamaMock.mockClear(); OllamaMock.mockClear();
// Create new service to trigger client creation // Create new service to trigger client creation
const newService = new OllamaService(); const newService = new OllamaService();
// Replace the formatter with a mock for the new service // Replace the formatter with a mock for the new service
(newService as any).formatter = { (newService as any).formatter = {
formatMessages: vi.fn().mockReturnValue([ formatMessages: vi.fn().mockReturnValue([
{ role: 'user', content: 'Hello' } { role: 'user', content: 'Hello' }
]) ])
}; };
await newService.generateChatCompletion(messages); await newService.generateChatCompletion(messages);
expect(OllamaMock).toHaveBeenCalledWith({ expect(OllamaMock).toHaveBeenCalledWith({
host: 'http://localhost:11434', host: 'http://localhost:11434',
fetch: expect.any(Function) fetch: expect.any(Function)
@ -417,7 +421,7 @@ describe('OllamaService', () => {
it('should handle tool execution feedback', async () => { it('should handle tool execution feedback', async () => {
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const mockOptions = { const mockOptions = {
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
model: 'llama2', model: 'llama2',
@ -426,7 +430,7 @@ describe('OllamaService', () => {
tools: [{ name: 'test_tool', description: 'Test', parameters: {} }] tools: [{ name: 'test_tool', description: 'Test', parameters: {} }]
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Mock response with tool call (arguments should be a string for Ollama) // Mock response with tool call (arguments should be a string for Ollama)
mockOllamaInstance.chat.mockResolvedValueOnce({ mockOllamaInstance.chat.mockResolvedValueOnce({
message: { message: {
@ -442,9 +446,9 @@ describe('OllamaService', () => {
}, },
done: true done: true
}); });
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result.tool_calls).toEqual([{ expect(result.tool_calls).toEqual([{
id: 'call_123', id: 'call_123',
type: 'function', type: 'function',
@ -457,14 +461,14 @@ describe('OllamaService', () => {
it('should handle mixed text and tool content', async () => { it('should handle mixed text and tool content', async () => {
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const mockOptions = { const mockOptions = {
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Mock response with both text and tool calls // Mock response with both text and tool calls
mockOllamaInstance.chat.mockResolvedValueOnce({ mockOllamaInstance.chat.mockResolvedValueOnce({
message: { message: {
@ -480,30 +484,30 @@ describe('OllamaService', () => {
}, },
done: true done: true
}); });
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result.text).toBe('Let me help you with that.'); expect(result.text).toBe('Let me help you with that.');
expect(result.tool_calls).toHaveLength(1); expect(result.tool_calls).toHaveLength(1);
}); });
it('should format messages using the formatter', async () => { it('should format messages using the formatter', async () => {
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const mockOptions = { const mockOptions = {
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
const formattedMessages = [{ role: 'user', content: 'Hello' }]; const formattedMessages = [{ role: 'user', content: 'Hello' }];
(service as any).formatter.formatMessages.mockReturnValueOnce(formattedMessages); (service as any).formatter.formatMessages.mockReturnValueOnce(formattedMessages);
const chatSpy = vi.spyOn(mockOllamaInstance, 'chat'); const chatSpy = vi.spyOn(mockOllamaInstance, 'chat');
await service.generateChatCompletion(messages); await service.generateChatCompletion(messages);
expect((service as any).formatter.formatMessages).toHaveBeenCalled(); expect((service as any).formatter.formatMessages).toHaveBeenCalled();
expect(chatSpy).toHaveBeenCalledWith( expect(chatSpy).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -514,23 +518,23 @@ describe('OllamaService', () => {
it('should handle network errors gracefully', async () => { it('should handle network errors gracefully', async () => {
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const mockOptions = { const mockOptions = {
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Mock network error // Mock network error
global.fetch = vi.fn().mockRejectedValueOnce( global.fetch = vi.fn().mockRejectedValueOnce(
new Error('Network error') new Error('Network error')
); );
mockOllamaInstance.chat.mockRejectedValueOnce( mockOllamaInstance.chat.mockRejectedValueOnce(
new Error('fetch failed') new Error('fetch failed')
); );
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'fetch failed' 'fetch failed'
); );
@ -538,19 +542,19 @@ describe('OllamaService', () => {
it('should validate model availability', async () => { it('should validate model availability', async () => {
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const mockOptions = { const mockOptions = {
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
model: 'nonexistent-model', model: 'nonexistent-model',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Mock model not found error // Mock model not found error
mockOllamaInstance.chat.mockRejectedValueOnce( mockOllamaInstance.chat.mockRejectedValueOnce(
new Error('model "nonexistent-model" not found') new Error('model "nonexistent-model" not found')
); );
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'model "nonexistent-model" not found' 'model "nonexistent-model" not found'
); );
@ -561,23 +565,23 @@ describe('OllamaService', () => {
it('should reuse existing client', async () => { it('should reuse existing client', async () => {
vi.mocked(options.getOptionBool).mockReturnValue(true); vi.mocked(options.getOptionBool).mockReturnValue(true);
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434'); vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
const mockOptions = { const mockOptions = {
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockResolvedValue(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValue(mockOptions);
const OllamaMock = vi.mocked(Ollama); const OllamaMock = vi.mocked(Ollama);
OllamaMock.mockClear(); OllamaMock.mockClear();
// Make two calls // Make two calls
await service.generateChatCompletion([{ role: 'user', content: 'Hello' }]); await service.generateChatCompletion([{ role: 'user', content: 'Hello' }]);
await service.generateChatCompletion([{ role: 'user', content: 'Hi' }]); await service.generateChatCompletion([{ role: 'user', content: 'Hi' }]);
// Should only create client once // Should only create client once
expect(OllamaMock).toHaveBeenCalledTimes(1); expect(OllamaMock).toHaveBeenCalledTimes(1);
}); });
}); });
}); });

View File

@ -2,13 +2,17 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { OpenAIService } from './openai_service.js'; import { OpenAIService } from './openai_service.js';
import options from '../../options.js'; import options from '../../options.js';
import * as providers from './providers.js'; import * as providers from './providers.js';
import type { ChatCompletionOptions, Message } from '../ai_interface.js'; import type { Message } from '../ai_interface.js';
// Mock dependencies // Mock dependencies
vi.mock('../../options.js', () => ({ vi.mock('../../options.js', () => ({
default: { default: {
getOption: vi.fn(), 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', () => { describe('isAvailable', () => {
it('should return true when base checks pass', () => { it('should return true when base checks pass', () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
const result = service.isAvailable(); const result = service.isAvailable();
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false when AI is disabled', () => { it('should return false when AI is disabled', () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
const result = service.isAvailable(); const result = service.isAvailable();
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
@ -89,7 +93,7 @@ describe('OpenAIService', () => {
enableTools: false enableTools: false
}; };
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
// Mock the getClient method to return our mock client // Mock the getClient method to return our mock client
const mockCompletion = { const mockCompletion = {
id: 'chatcmpl-123', id: 'chatcmpl-123',
@ -120,9 +124,9 @@ describe('OpenAIService', () => {
}; };
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result).toEqual({ expect(result).toEqual({
text: 'Hello! How can I help you today?', text: 'Hello! How can I help you today?',
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
@ -144,7 +148,7 @@ describe('OpenAIService', () => {
stream: true stream: true
}; };
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
// Mock the streaming response // Mock the streaming response
const mockStream = { const mockStream = {
[Symbol.asyncIterator]: async function* () { [Symbol.asyncIterator]: async function* () {
@ -162,7 +166,7 @@ describe('OpenAIService', () => {
}; };
} }
}; };
const mockClient = { const mockClient = {
chat: { chat: {
completions: { completions: {
@ -172,9 +176,9 @@ describe('OpenAIService', () => {
}; };
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result).toHaveProperty('stream'); expect(result).toHaveProperty('stream');
expect(result.text).toBe(''); expect(result.text).toBe('');
expect(result.model).toBe('gpt-3.5-turbo'); expect(result.model).toBe('gpt-3.5-turbo');
@ -183,7 +187,7 @@ describe('OpenAIService', () => {
it('should throw error if service not available', async () => { it('should throw error if service not available', async () => {
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'OpenAI service is not available' 'OpenAI service is not available'
); );
@ -197,7 +201,7 @@ describe('OpenAIService', () => {
stream: false stream: false
}; };
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
const mockClient = { const mockClient = {
chat: { chat: {
completions: { completions: {
@ -207,7 +211,7 @@ describe('OpenAIService', () => {
}; };
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'API Error: Invalid API key' 'API Error: Invalid API key'
); );
@ -222,7 +226,7 @@ describe('OpenAIService', () => {
parameters: {} parameters: {}
} }
}]; }];
const mockOptions = { const mockOptions = {
apiKey: 'test-key', apiKey: 'test-key',
baseUrl: 'https://api.openai.com/v1', baseUrl: 'https://api.openai.com/v1',
@ -233,7 +237,7 @@ describe('OpenAIService', () => {
tool_choice: 'auto' tool_choice: 'auto'
}; };
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
const mockCompletion = { const mockCompletion = {
id: 'chatcmpl-123', id: 'chatcmpl-123',
object: 'chat.completion', object: 'chat.completion',
@ -263,9 +267,9 @@ describe('OpenAIService', () => {
}; };
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
await service.generateChatCompletion(messages); await service.generateChatCompletion(messages);
const createCall = mockClient.chat.completions.create.mock.calls[0][0]; const createCall = mockClient.chat.completions.create.mock.calls[0][0];
expect(createCall.tools).toEqual(mockTools); expect(createCall.tools).toEqual(mockTools);
expect(createCall.tool_choice).toBe('auto'); expect(createCall.tool_choice).toBe('auto');
@ -281,7 +285,7 @@ describe('OpenAIService', () => {
tools: [{ type: 'function' as const, function: { name: 'test', description: 'test' } }] tools: [{ type: 'function' as const, function: { name: 'test', description: 'test' } }]
}; };
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
const mockCompletion = { const mockCompletion = {
id: 'chatcmpl-123', id: 'chatcmpl-123',
object: 'chat.completion', object: 'chat.completion',
@ -319,9 +323,9 @@ describe('OpenAIService', () => {
}; };
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient); vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result).toEqual({ expect(result).toEqual({
text: '', text: '',
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
@ -342,4 +346,4 @@ describe('OpenAIService', () => {
}); });
}); });
}); });
}); });