fix(unit): no more type errors hopefully

This commit is contained in:
perf3ct 2025-06-08 16:33:26 +00:00
parent d7abd3a8ed
commit e011c56715
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
12 changed files with 384 additions and 487 deletions

View File

@ -198,7 +198,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(null); // 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'
@ -216,7 +216,7 @@ describe('AIServiceManager', () => {
}); });
it('should return false if no providers are available', () => { it('should return false if no providers are available', () => {
vi.mocked(options.getOption).mockReturnValue(null); vi.mocked(options.getOption).mockReturnValue('');
const result = manager.isAnyServiceAvailable(); const result = manager.isAnyServiceAvailable();
@ -229,7 +229,7 @@ describe('AIServiceManager', () => {
vi.mocked(options.getOption) vi.mocked(options.getOption)
.mockReturnValueOnce('openai-key') .mockReturnValueOnce('openai-key')
.mockReturnValueOnce('anthropic-key') .mockReturnValueOnce('anthropic-key')
.mockReturnValueOnce(null); // No Ollama URL .mockReturnValueOnce(''); // No Ollama URL
const result = manager.getAvailableProviders(); const result = manager.getAvailableProviders();
@ -274,7 +274,8 @@ describe('AIServiceManager', () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
vi.mocked(configHelpers.parseModelIdentifier).mockReturnValueOnce({ vi.mocked(configHelpers.parseModelIdentifier).mockReturnValueOnce({
provider: 'openai', provider: 'openai',
modelId: 'gpt-4' modelId: 'gpt-4',
fullIdentifier: 'openai:gpt-4'
}); });
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
@ -314,7 +315,8 @@ describe('AIServiceManager', () => {
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai'); vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
vi.mocked(configHelpers.parseModelIdentifier).mockReturnValueOnce({ vi.mocked(configHelpers.parseModelIdentifier).mockReturnValueOnce({
provider: 'anthropic', provider: 'anthropic',
modelId: 'claude-3' modelId: 'claude-3',
fullIdentifier: 'anthropic:claude-3'
}); });
await expect( await expect(
@ -396,7 +398,7 @@ describe('AIServiceManager', () => {
}); });
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(null); // 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'
@ -414,7 +416,7 @@ describe('AIServiceManager', () => {
}); });
it('should return default provider if none selected', () => { it('should return default provider if none selected', () => {
vi.mocked(options.getOption).mockReturnValueOnce(null); vi.mocked(options.getOption).mockReturnValueOnce('');
const result = manager.getSelectedProvider(); const result = manager.getSelectedProvider();

View File

@ -57,7 +57,7 @@ vi.mock('../config/configuration_helpers.js', () => ({
})); }));
describe('RestChatService', () => { describe('RestChatService', () => {
let restChatService: RestChatService; let restChatService: typeof RestChatService;
let mockOptions: any; let mockOptions: any;
let mockAiServiceManager: any; let mockAiServiceManager: any;
let mockChatStorageService: any; let mockChatStorageService: any;

View File

@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ChatStorageService } from './chat_storage_service.js'; import { ChatStorageService, type StoredChat } from './chat_storage_service.js';
import type { Message } from './ai_interface.js'; import type { Message } from './ai_interface.js';
// Mock dependencies // Mock dependencies
@ -307,10 +307,10 @@ describe('ChatStorageService', () => {
describe('updateChat', () => { describe('updateChat', () => {
it('should update chat messages and metadata', async () => { it('should update chat messages and metadata', async () => {
const existingChat = { const existingChat: StoredChat = {
id: 'chat-123', id: 'chat-123',
title: 'Test Chat', title: 'Test Chat',
messages: [{ role: 'user', content: 'Hello' }], messages: [{ role: 'user' as const, content: 'Hello' }],
noteId: 'chat-123', noteId: 'chat-123',
createdAt: new Date('2024-01-01T00:00:00Z'), createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T01:00:00Z'), updatedAt: new Date('2024-01-01T01:00:00Z'),

View File

@ -6,7 +6,7 @@ import type { ToolCall } from './tools/tool_interfaces.js';
import { t } from 'i18next'; import { t } from 'i18next';
import log from '../log.js'; import log from '../log.js';
interface StoredChat { export interface StoredChat {
id: string; id: string;
title: string; title: string;
messages: Message[]; messages: Message[];

View File

@ -10,7 +10,8 @@ vi.mock('./configuration_manager.js', () => ({
parseModelIdentifier: vi.fn(), parseModelIdentifier: vi.fn(),
createModelConfig: vi.fn(), createModelConfig: vi.fn(),
getAIConfig: vi.fn(), getAIConfig: vi.fn(),
validateConfiguration: vi.fn() validateConfig: vi.fn(),
clearCache: vi.fn()
} }
})); }));
@ -48,7 +49,7 @@ describe('configuration_helpers', () => {
}); });
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(null); vi.mocked(optionService.getOption).mockReturnValueOnce('');
const result = await configHelpers.getSelectedProvider(); const result = await configHelpers.getSelectedProvider();
@ -68,7 +69,8 @@ describe('configuration_helpers', () => {
it('should delegate to configuration manager', () => { it('should delegate to configuration manager', () => {
const mockIdentifier: ModelIdentifier = { const mockIdentifier: ModelIdentifier = {
provider: 'openai', provider: 'openai',
modelId: 'gpt-4' modelId: 'gpt-4',
fullIdentifier: 'openai:gpt-4'
}; };
vi.mocked(configurationManager.parseModelIdentifier).mockReturnValueOnce(mockIdentifier); vi.mocked(configurationManager.parseModelIdentifier).mockReturnValueOnce(mockIdentifier);
@ -83,10 +85,10 @@ describe('configuration_helpers', () => {
it('should delegate to configuration manager', () => { it('should delegate to configuration manager', () => {
const mockConfig: ModelConfig = { const mockConfig: ModelConfig = {
provider: 'openai', provider: 'openai',
model: 'gpt-4', modelId: 'gpt-4',
temperature: 0.7, temperature: 0.7,
maxTokens: 1000 maxTokens: 1000
}; } as any;
vi.mocked(configurationManager.createModelConfig).mockReturnValueOnce(mockConfig); vi.mocked(configurationManager.createModelConfig).mockReturnValueOnce(mockConfig);
const result = configHelpers.createModelConfig('gpt-4', 'openai'); const result = configHelpers.createModelConfig('gpt-4', 'openai');
@ -291,7 +293,7 @@ describe('configuration_helpers', () => {
}); });
it('should return null if no provider selected', async () => { it('should return null if no provider selected', async () => {
vi.mocked(optionService.getOption).mockReturnValueOnce(null); vi.mocked(optionService.getOption).mockReturnValueOnce('');
const result = await configHelpers.getAvailableSelectedProvider(); const result = await configHelpers.getAvailableSelectedProvider();
@ -322,20 +324,19 @@ describe('configuration_helpers', () => {
errors: [], errors: [],
warnings: [] warnings: []
}; };
vi.mocked(configurationManager.validateConfiguration).mockResolvedValueOnce(mockValidation); vi.mocked(configurationManager.validateConfig).mockResolvedValueOnce(mockValidation);
const result = await configHelpers.validateConfiguration(); const result = await configHelpers.validateConfiguration();
expect(result).toBe(mockValidation); expect(result).toBe(mockValidation);
expect(configurationManager.validateConfiguration).toHaveBeenCalled(); expect(configurationManager.validateConfig).toHaveBeenCalled();
}); });
}); });
describe('clearConfigurationCache', () => { describe('clearConfigurationCache', () => {
it('should delegate to configuration manager', () => { it('should clear configuration cache (no-op)', () => {
configHelpers.clearConfigurationCache(); // The function is now a no-op since caching was removed
expect(() => configHelpers.clearConfigurationCache()).not.toThrow();
expect(configurationManager.clearConfigurationCache).toHaveBeenCalled();
}); });
}); });
}); });

View File

@ -23,7 +23,7 @@ vi.mock('../modules/cache_manager.js', () => ({
vi.mock('./query_processor.js', () => ({ vi.mock('./query_processor.js', () => ({
default: { default: {
enhanceQuery: vi.fn().mockResolvedValue('enhanced query'), generateSearchQueries: vi.fn().mockResolvedValue(['search query 1', 'search query 2']),
decomposeQuery: vi.fn().mockResolvedValue({ decomposeQuery: vi.fn().mockResolvedValue({
subQueries: ['sub query 1', 'sub query 2'], subQueries: ['sub query 1', 'sub query 2'],
thinking: 'decomposition thinking' thinking: 'decomposition thinking'
@ -33,8 +33,8 @@ vi.mock('./query_processor.js', () => ({
vi.mock('../modules/context_formatter.js', () => ({ vi.mock('../modules/context_formatter.js', () => ({
default: { default: {
formatNotes: vi.fn().mockReturnValue('formatted context'), buildContextFromNotes: vi.fn().mockResolvedValue('formatted context'),
formatResponse: vi.fn().mockReturnValue('formatted response') sanitizeNoteContent: vi.fn().mockReturnValue('sanitized content')
} }
})); }));
@ -64,8 +64,7 @@ describe('ContextService', () => {
generateChatCompletion: vi.fn().mockResolvedValue({ generateChatCompletion: vi.fn().mockResolvedValue({
content: 'Mock LLM response', content: 'Mock LLM response',
role: 'assistant' role: 'assistant'
}), })
isAvailable: vi.fn().mockReturnValue(true)
}; };
}); });
@ -134,8 +133,7 @@ describe('ContextService', () => {
noteId: 'note1', noteId: 'note1',
title: 'Features Overview', title: 'Features Overview',
content: 'The app has many features...', content: 'The app has many features...',
relevanceScore: 0.9, similarity: 0.9
searchType: 'content'
} }
]; ];
@ -162,8 +160,7 @@ describe('ContextService', () => {
noteId: 'note1', noteId: 'note1',
title: 'Long Content', title: 'Long Content',
content: 'This is a very long piece of content that should be summarized...', content: 'This is a very long piece of content that should be summarized...',
relevanceScore: 0.8, similarity: 0.8
searchType: 'content'
} }
]; ];
@ -183,7 +180,7 @@ describe('ContextService', () => {
); );
}); });
it('should handle query enhancement option', async () => { it('should handle query generation option', async () => {
const options: ContextOptions = { const options: ContextOptions = {
useQueryEnhancement: true useQueryEnhancement: true
}; };
@ -192,7 +189,7 @@ describe('ContextService', () => {
await service.processQuery(userQuestion, mockLLMService, options); await service.processQuery(userQuestion, mockLLMService, options);
expect(queryProcessor.enhanceQuery).toHaveBeenCalledWith( expect(queryProcessor.generateSearchQueries).toHaveBeenCalledWith(
userQuestion, userQuestion,
mockLLMService mockLLMService
); );
@ -262,13 +259,13 @@ describe('ContextService', () => {
}; };
const queryProcessor = (await import('./query_processor.js')).default; const queryProcessor = (await import('./query_processor.js')).default;
queryProcessor.enhanceQuery.mockRejectedValueOnce( vi.mocked(queryProcessor.generateSearchQueries).mockRejectedValueOnce(
new Error('Query enhancement failed') new Error('Query generation failed')
); );
await expect( await expect(
service.processQuery(userQuestion, mockLLMService, options) service.processQuery(userQuestion, mockLLMService, options)
).rejects.toThrow('Query enhancement failed'); ).rejects.toThrow('Query generation failed');
}); });
it('should handle errors in query decomposition', async () => { it('should handle errors in query decomposition', async () => {
@ -277,7 +274,7 @@ describe('ContextService', () => {
}; };
const queryProcessor = (await import('./query_processor.js')).default; const queryProcessor = (await import('./query_processor.js')).default;
queryProcessor.decomposeQuery.mockRejectedValueOnce( vi.mocked(queryProcessor.decomposeQuery).mockRejectedValueOnce(
new Error('Query decomposition failed') new Error('Query decomposition failed')
); );
@ -298,8 +295,7 @@ describe('ContextService', () => {
noteId: 'note1', noteId: 'note1',
title: 'Relevant Note', title: 'Relevant Note',
content: 'This note is relevant to the query', content: 'This note is relevant to the query',
relevanceScore: 0.85, similarity: 0.85
searchType: 'content'
} }
]; ];
@ -377,9 +373,9 @@ describe('ContextService', () => {
await service.initialize(); await service.initialize();
const contextFormatter = (await import('../modules/context_formatter.js')).default; const contextFormatter = (await import('../modules/context_formatter.js')).default;
contextFormatter.formatNotes.mockImplementationOnce(() => { vi.mocked(contextFormatter.buildContextFromNotes).mockRejectedValueOnce(
throw new Error('Formatting error'); new Error('Formatting error')
}); );
await expect( await expect(
service.processQuery('test', mockLLMService) service.processQuery('test', mockLLMService)
@ -397,8 +393,7 @@ describe('ContextService', () => {
noteId: `note${i}`, noteId: `note${i}`,
title: `Note ${i}`, title: `Note ${i}`,
content: `Content for note ${i}`, content: `Content for note ${i}`,
relevanceScore: Math.random(), similarity: Math.random()
searchType: 'content' as const
})); }));
(service as any).contextExtractor.findRelevantNotes.mockResolvedValueOnce(largeResultSet); (service as any).contextExtractor.findRelevantNotes.mockResolvedValueOnce(largeResultSet);

View File

@ -13,32 +13,32 @@ vi.mock('../log.js', () => ({
vi.mock('./interfaces/model_capabilities.js', () => ({ vi.mock('./interfaces/model_capabilities.js', () => ({
DEFAULT_MODEL_CAPABILITIES: { DEFAULT_MODEL_CAPABILITIES: {
contextLength: 4096, contextWindowTokens: 8192,
supportedMessageTypes: ['text'], contextWindowChars: 16000,
supportsToolCalls: false, maxCompletionTokens: 1024,
supportsStreaming: true, hasFunctionCalling: false,
maxOutputTokens: 2048, hasVision: false,
temperature: { min: 0, max: 2, default: 0.7 }, costPerInputToken: 0,
topP: { min: 0, max: 1, default: 0.9 } costPerOutputToken: 0
} }
})); }));
vi.mock('./constants/search_constants.js', () => ({ vi.mock('./constants/search_constants.js', () => ({
MODEL_CAPABILITIES: { MODEL_CAPABILITIES: {
'gpt-4': { 'gpt-4': {
contextLength: 8192, contextWindowTokens: 8192,
supportsToolCalls: true, contextWindowChars: 32000,
maxOutputTokens: 4096 hasFunctionCalling: true
}, },
'gpt-3.5-turbo': { 'gpt-3.5-turbo': {
contextLength: 4096, contextWindowTokens: 8192,
supportsToolCalls: true, contextWindowChars: 16000,
maxOutputTokens: 2048 hasFunctionCalling: true
}, },
'claude-3-opus': { 'claude-3-opus': {
contextLength: 200000, contextWindowTokens: 200000,
supportsToolCalls: true, contextWindowChars: 800000,
maxOutputTokens: 4096 hasVision: true
} }
} }
})); }));
@ -69,13 +69,13 @@ describe('ModelCapabilitiesService', () => {
describe('getChatModelCapabilities', () => { describe('getChatModelCapabilities', () => {
it('should return cached capabilities if available', async () => { it('should return cached capabilities if available', async () => {
const mockCapabilities: ModelCapabilities = { const mockCapabilities: ModelCapabilities = {
contextLength: 8192, contextWindowTokens: 8192,
supportedMessageTypes: ['text'], contextWindowChars: 32000,
supportsToolCalls: true, maxCompletionTokens: 4096,
supportsStreaming: true, hasFunctionCalling: true,
maxOutputTokens: 4096, hasVision: false,
temperature: { min: 0, max: 2, default: 0.7 }, costPerInputToken: 0,
topP: { min: 0, max: 1, default: 0.9 } costPerOutputToken: 0
}; };
// Pre-populate cache // Pre-populate cache
@ -91,13 +91,13 @@ describe('ModelCapabilitiesService', () => {
const result = await service.getChatModelCapabilities('gpt-4'); const result = await service.getChatModelCapabilities('gpt-4');
expect(result).toEqual({ expect(result).toEqual({
contextLength: 8192, contextWindowTokens: 8192,
supportedMessageTypes: ['text'], contextWindowChars: 32000,
supportsToolCalls: true, maxCompletionTokens: 1024,
supportsStreaming: true, hasFunctionCalling: true,
maxOutputTokens: 4096, hasVision: false,
temperature: { min: 0, max: 2, default: 0.7 }, costPerInputToken: 0,
topP: { min: 0, max: 1, default: 0.9 } costPerOutputToken: 0
}); });
expect(mockLog.info).toHaveBeenCalledWith('Using static capabilities for chat model: gpt-4'); expect(mockLog.info).toHaveBeenCalledWith('Using static capabilities for chat model: gpt-4');
@ -110,8 +110,8 @@ describe('ModelCapabilitiesService', () => {
it('should handle case-insensitive model names', async () => { it('should handle case-insensitive model names', async () => {
const result = await service.getChatModelCapabilities('GPT-4'); const result = await service.getChatModelCapabilities('GPT-4');
expect(result.contextLength).toBe(8192); expect(result.contextWindowTokens).toBe(8192);
expect(result.supportsToolCalls).toBe(true); expect(result.hasFunctionCalling).toBe(true);
expect(mockLog.info).toHaveBeenCalledWith('Using static capabilities for chat model: GPT-4'); expect(mockLog.info).toHaveBeenCalledWith('Using static capabilities for chat model: GPT-4');
}); });
@ -119,9 +119,9 @@ describe('ModelCapabilitiesService', () => {
const result = await service.getChatModelCapabilities('unknown-model'); const result = await service.getChatModelCapabilities('unknown-model');
expect(result).toEqual({ expect(result).toEqual({
contextLength: 4096, contextWindow: 4096,
supportedMessageTypes: ['text'], supportedMessageTypes: ['text'],
supportsToolCalls: false, supportsTools: false,
supportsStreaming: true, supportsStreaming: true,
maxOutputTokens: 2048, maxOutputTokens: 2048,
temperature: { min: 0, max: 2, default: 0.7 }, temperature: { min: 0, max: 2, default: 0.7 },
@ -135,9 +135,9 @@ describe('ModelCapabilitiesService', () => {
const result = await service.getChatModelCapabilities('gpt-3.5-turbo'); const result = await service.getChatModelCapabilities('gpt-3.5-turbo');
expect(result).toEqual({ expect(result).toEqual({
contextLength: 4096, contextWindow: 4096,
supportedMessageTypes: ['text'], supportedMessageTypes: ['text'],
supportsToolCalls: true, supportsTools: true,
supportsStreaming: true, supportsStreaming: true,
maxOutputTokens: 2048, maxOutputTokens: 2048,
temperature: { min: 0, max: 2, default: 0.7 }, temperature: { min: 0, max: 2, default: 0.7 },
@ -150,13 +150,13 @@ describe('ModelCapabilitiesService', () => {
describe('clearCache', () => { describe('clearCache', () => {
it('should clear all cached capabilities', () => { it('should clear all cached capabilities', () => {
const mockCapabilities: ModelCapabilities = { const mockCapabilities: ModelCapabilities = {
contextLength: 8192, contextWindowTokens: 8192,
supportedMessageTypes: ['text'], contextWindowChars: 32000,
supportsToolCalls: true, maxCompletionTokens: 4096,
supportsStreaming: true, hasFunctionCalling: true,
maxOutputTokens: 4096, hasVision: false,
temperature: { min: 0, max: 2, default: 0.7 }, costPerInputToken: 0,
topP: { min: 0, max: 1, default: 0.9 } costPerOutputToken: 0
}; };
// Pre-populate cache // Pre-populate cache
@ -175,23 +175,23 @@ describe('ModelCapabilitiesService', () => {
describe('getCachedCapabilities', () => { describe('getCachedCapabilities', () => {
it('should return all cached capabilities as a record', () => { it('should return all cached capabilities as a record', () => {
const mockCapabilities1: ModelCapabilities = { const mockCapabilities1: ModelCapabilities = {
contextLength: 8192, contextWindowTokens: 8192,
supportedMessageTypes: ['text'], contextWindowChars: 32000,
supportsToolCalls: true, maxCompletionTokens: 4096,
supportsStreaming: true, hasFunctionCalling: true,
maxOutputTokens: 4096, hasVision: false,
temperature: { min: 0, max: 2, default: 0.7 }, costPerInputToken: 0,
topP: { min: 0, max: 1, default: 0.9 } costPerOutputToken: 0
}; };
const mockCapabilities2: ModelCapabilities = { const mockCapabilities2: ModelCapabilities = {
contextLength: 4096, contextWindowTokens: 8192,
supportedMessageTypes: ['text'], contextWindowChars: 16000,
supportsToolCalls: false, maxCompletionTokens: 1024,
supportsStreaming: true, hasFunctionCalling: false,
maxOutputTokens: 2048, hasVision: false,
temperature: { min: 0, max: 2, default: 0.7 }, costPerInputToken: 0,
topP: { min: 0, max: 1, default: 0.9 } costPerOutputToken: 0
}; };
// Pre-populate cache // Pre-populate cache
@ -220,9 +220,9 @@ describe('ModelCapabilitiesService', () => {
const result = await fetchMethod('claude-3-opus'); const result = await fetchMethod('claude-3-opus');
expect(result).toEqual({ expect(result).toEqual({
contextLength: 200000, contextWindow: 200000,
supportedMessageTypes: ['text'], supportedMessageTypes: ['text'],
supportsToolCalls: true, supportsTools: true,
supportsStreaming: true, supportsStreaming: true,
maxOutputTokens: 4096, maxOutputTokens: 4096,
temperature: { min: 0, max: 2, default: 0.7 }, temperature: { min: 0, max: 2, default: 0.7 },
@ -237,9 +237,9 @@ describe('ModelCapabilitiesService', () => {
const result = await fetchMethod('unknown-model'); const result = await fetchMethod('unknown-model');
expect(result).toEqual({ expect(result).toEqual({
contextLength: 4096, contextWindow: 4096,
supportedMessageTypes: ['text'], supportedMessageTypes: ['text'],
supportsToolCalls: false, supportsTools: false,
supportsStreaming: true, supportsStreaming: true,
maxOutputTokens: 2048, maxOutputTokens: 2048,
temperature: { min: 0, max: 2, default: 0.7 }, temperature: { min: 0, max: 2, default: 0.7 },
@ -260,9 +260,9 @@ describe('ModelCapabilitiesService', () => {
const result = await fetchMethod('test-model'); const result = await fetchMethod('test-model');
expect(result).toEqual({ expect(result).toEqual({
contextLength: 4096, contextWindow: 4096,
supportedMessageTypes: ['text'], supportedMessageTypes: ['text'],
supportsToolCalls: false, supportsTools: false,
supportsStreaming: true, supportsStreaming: true,
maxOutputTokens: 2048, maxOutputTokens: 2048,
temperature: { min: 0, max: 2, default: 0.7 }, temperature: { min: 0, max: 2, default: 0.7 },

View File

@ -188,8 +188,8 @@ describe('ChatPipeline', () => {
const input: ChatPipelineInput = { const input: ChatPipelineInput = {
messages, messages,
noteId: 'note-123', options: {},
userId: 'user-456' noteId: 'note-123'
}; };
it('should execute all pipeline stages in order', async () => { it('should execute all pipeline stages in order', async () => {
@ -235,13 +235,16 @@ describe('ChatPipeline', () => {
it('should handle tool calling iterations', async () => { it('should handle tool calling iterations', async () => {
// Mock tool calling stage to require a tool call // Mock tool calling stage to require a tool call
const mockToolCallingStage = pipeline.stages.toolCalling; const mockToolCallingStage = pipeline.stages.toolCalling;
mockToolCallingStage.execute vi.mocked(mockToolCallingStage.execute)
.mockResolvedValueOnce({ .mockResolvedValueOnce({
toolCallRequired: true, response: { text: 'Using tool...', model: 'test', provider: 'test' },
toolCallMessages: [{ role: 'assistant', content: 'Using tool...' }] needsFollowUp: true,
messages: [{ role: 'assistant', content: 'Using tool...' }]
}) })
.mockResolvedValueOnce({ .mockResolvedValueOnce({
toolCallRequired: false response: { text: 'Done', model: 'test', provider: 'test' },
needsFollowUp: false,
messages: []
}); });
await pipeline.execute(input); await pipeline.execute(input);
@ -256,9 +259,10 @@ describe('ChatPipeline', () => {
// Mock tool calling stage to always require tool calls // Mock tool calling stage to always require tool calls
const mockToolCallingStage = pipeline.stages.toolCalling; const mockToolCallingStage = pipeline.stages.toolCalling;
mockToolCallingStage.execute.mockResolvedValue({ vi.mocked(mockToolCallingStage.execute).mockResolvedValue({
toolCallRequired: true, response: { text: 'Using tool...', model: 'test', provider: 'test' },
toolCallMessages: [{ role: 'assistant', content: 'Using tool...' }] needsFollowUp: true,
messages: [{ role: 'assistant', content: 'Using tool...' }]
}); });
await pipeline.execute(input); await pipeline.execute(input);
@ -269,7 +273,7 @@ describe('ChatPipeline', () => {
it('should handle stage errors gracefully', async () => { it('should handle stage errors gracefully', async () => {
// Mock a stage to throw an error // Mock a stage to throw an error
pipeline.stages.contextExtraction.execute.mockRejectedValueOnce( vi.mocked(pipeline.stages.contextExtraction.execute).mockRejectedValueOnce(
new Error('Context extraction failed') new Error('Context extraction failed')
); );
@ -279,8 +283,8 @@ describe('ChatPipeline', () => {
}); });
it('should pass context between stages', async () => { it('should pass context between stages', async () => {
const contextData = { relevantNotes: ['note1', 'note2'] }; const contextData = { context: 'Note context', noteId: 'note-123', query: 'test query' };
pipeline.stages.contextExtraction.execute.mockResolvedValueOnce(contextData); vi.mocked(pipeline.stages.contextExtraction.execute).mockResolvedValueOnce(contextData);
await pipeline.execute(input); await pipeline.execute(input);
@ -292,8 +296,8 @@ describe('ChatPipeline', () => {
it('should handle empty messages', async () => { it('should handle empty messages', async () => {
const emptyInput: ChatPipelineInput = { const emptyInput: ChatPipelineInput = {
messages: [], messages: [],
noteId: 'note-123', options: {},
userId: 'user-456' noteId: 'note-123'
}; };
const result = await pipeline.execute(emptyInput); const result = await pipeline.execute(emptyInput);
@ -365,8 +369,8 @@ describe('ChatPipeline', () => {
const input: ChatPipelineInput = { const input: ChatPipelineInput = {
messages: [{ role: 'user', content: 'Hello' }], messages: [{ role: 'user', content: 'Hello' }],
noteId: 'note-123', options: {},
userId: 'user-456' noteId: 'note-123'
}; };
await pipeline.execute(input); await pipeline.execute(input);
@ -381,8 +385,8 @@ describe('ChatPipeline', () => {
const input: ChatPipelineInput = { const input: ChatPipelineInput = {
messages: [{ role: 'user', content: 'Hello' }], messages: [{ role: 'user', content: 'Hello' }],
noteId: 'note-123', options: {},
userId: 'user-456' noteId: 'note-123'
}; };
await pipeline.execute(input); await pipeline.execute(input);
@ -395,12 +399,12 @@ describe('ChatPipeline', () => {
describe('error handling', () => { describe('error handling', () => {
it('should propagate errors from stages', async () => { it('should propagate errors from stages', async () => {
const error = new Error('Stage execution failed'); const error = new Error('Stage execution failed');
pipeline.stages.messagePreparation.execute.mockRejectedValueOnce(error); vi.mocked(pipeline.stages.messagePreparation.execute).mockRejectedValueOnce(error);
const input: ChatPipelineInput = { const input: ChatPipelineInput = {
messages: [{ role: 'user', content: 'Hello' }], messages: [{ role: 'user', content: 'Hello' }],
noteId: 'note-123', options: {},
userId: 'user-456' noteId: 'note-123'
}; };
await expect(pipeline.execute(input)).rejects.toThrow('Stage execution failed'); await expect(pipeline.execute(input)).rejects.toThrow('Stage execution failed');

View File

@ -117,7 +117,7 @@ describe('AnthropicService', () => {
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(null); // No API key vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
const result = service.isAvailable(); const result = service.isAvailable();
@ -170,7 +170,7 @@ describe('AnthropicService', () => {
await service.generateChatCompletion(messages); await service.generateChatCompletion(messages);
const calledParams = createSpy.mock.calls[0][0]; 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' }
]); ]);
@ -382,7 +382,7 @@ describe('AnthropicService', () => {
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result.content).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');
}); });
@ -418,7 +418,7 @@ describe('AnthropicService', () => {
await service.generateChatCompletion(messagesWithToolResult); await service.generateChatCompletion(messagesWithToolResult);
const formattedMessages = createSpy.mock.calls[0][0].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({
role: 'user', role: 'user',

View File

@ -161,7 +161,7 @@ describe('OllamaService', () => {
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(null); // No base URL vi.mocked(options.getOption).mockReturnValueOnce(''); // No base URL
const result = service.isAvailable(); const result = service.isAvailable();
@ -188,7 +188,7 @@ describe('OllamaService', () => {
temperature: 0.7, temperature: 0.7,
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
@ -207,7 +207,7 @@ describe('OllamaService', () => {
stream: true, stream: true,
onChunk: vi.fn() onChunk: vi.fn()
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
@ -240,13 +240,13 @@ describe('OllamaService', () => {
enableTools: true, enableTools: true,
tools: mockTools tools: mockTools
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(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]; const calledParams = chatSpy.mock.calls[0][0] as any;
expect(calledParams.tools).toEqual(mockTools); expect(calledParams.tools).toEqual(mockTools);
}); });
@ -259,7 +259,7 @@ describe('OllamaService', () => {
}); });
it('should throw error if no base URL configured', async () => { it('should throw error if no base URL configured', async () => {
vi.mocked(options.getOption).mockReturnValueOnce(null); // No base URL vi.mocked(options.getOption).mockReturnValueOnce(''); // No base URL
await expect(service.generateChatCompletion(messages)).rejects.toThrow( await expect(service.generateChatCompletion(messages)).rejects.toThrow(
'Ollama base URL is not configured' 'Ollama base URL is not configured'
@ -272,7 +272,7 @@ describe('OllamaService', () => {
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Mock API error // Mock API error
mockOllamaInstance.chat.mockRejectedValueOnce( mockOllamaInstance.chat.mockRejectedValueOnce(
@ -290,7 +290,7 @@ describe('OllamaService', () => {
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(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);
@ -314,7 +314,7 @@ describe('OllamaService', () => {
enableTools: true, enableTools: true,
tools: [{ name: 'test_tool', description: 'Test', parameters: {} }] tools: [{ name: 'test_tool', description: 'Test', parameters: {} }]
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Mock response with tool call // Mock response with tool call
mockOllamaInstance.chat.mockResolvedValueOnce({ mockOllamaInstance.chat.mockResolvedValueOnce({
@ -350,7 +350,7 @@ describe('OllamaService', () => {
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(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({
@ -370,7 +370,7 @@ describe('OllamaService', () => {
const result = await service.generateChatCompletion(messages); const result = await service.generateChatCompletion(messages);
expect(result.content).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);
}); });
@ -380,7 +380,7 @@ describe('OllamaService', () => {
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(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);
@ -406,7 +406,7 @@ describe('OllamaService', () => {
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Mock network error // Mock network error
global.fetch = vi.fn().mockRejectedValueOnce( global.fetch = vi.fn().mockRejectedValueOnce(
@ -428,7 +428,7 @@ describe('OllamaService', () => {
model: 'nonexistent-model', model: 'nonexistent-model',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValueOnce(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
// Mock model not found error // Mock model not found error
mockOllamaInstance.chat.mockRejectedValueOnce( mockOllamaInstance.chat.mockRejectedValueOnce(
@ -451,7 +451,7 @@ describe('OllamaService', () => {
model: 'llama2', model: 'llama2',
stream: false stream: false
}; };
vi.mocked(providers.getOllamaOptions).mockReturnValue(mockOptions); vi.mocked(providers.getOllamaOptions).mockResolvedValue(mockOptions);
const OllamaMock = vi.mocked(Ollama); const OllamaMock = vi.mocked(Ollama);
OllamaMock.mockClear(); OllamaMock.mockClear();

View File

@ -81,6 +81,7 @@ describe('OpenAIService', () => {
it('should generate non-streaming completion', async () => { it('should generate non-streaming completion', async () => {
const mockOptions = { const mockOptions = {
apiKey: 'test-key', apiKey: 'test-key',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
temperature: 0.7, temperature: 0.7,
max_tokens: 1000, max_tokens: 1000,
@ -138,6 +139,7 @@ describe('OpenAIService', () => {
it('should handle streaming completion', async () => { it('should handle streaming completion', async () => {
const mockOptions = { const mockOptions = {
apiKey: 'test-key', apiKey: 'test-key',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
stream: true stream: true
}; };
@ -190,6 +192,7 @@ describe('OpenAIService', () => {
it('should handle API errors', async () => { it('should handle API errors', async () => {
const mockOptions = { const mockOptions = {
apiKey: 'test-key', apiKey: 'test-key',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
stream: false stream: false
}; };
@ -222,6 +225,7 @@ describe('OpenAIService', () => {
const mockOptions = { const mockOptions = {
apiKey: 'test-key', apiKey: 'test-key',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
stream: false, stream: false,
enableTools: true, enableTools: true,
@ -270,6 +274,7 @@ describe('OpenAIService', () => {
it('should handle tool calls in response', async () => { it('should handle tool calls in response', async () => {
const mockOptions = { const mockOptions = {
apiKey: 'test-key', apiKey: 'test-key',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
stream: false, stream: false,
enableTools: true, enableTools: true,

View File

@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ToolRegistry } from './tool_registry.js'; import { ToolRegistry } from './tool_registry.js';
import type { Tool, ToolHandler } from './tool_interfaces.js'; import type { ToolHandler } from './tool_interfaces.js';
// Mock dependencies // Mock dependencies
vi.mock('../../log.js', () => ({ vi.mock('../../log.js', () => ({
@ -21,7 +21,6 @@ describe('ToolRegistry', () => {
// Clear any existing tools // Clear any existing tools
(registry as any).tools.clear(); (registry as any).tools.clear();
(registry as any).initializationAttempted = false;
vi.clearAllMocks(); vi.clearAllMocks();
}); });
@ -48,8 +47,8 @@ describe('ToolRegistry', () => {
}); });
}); });
describe('tool validation', () => { describe('registerTool', () => {
it('should validate a proper tool handler', () => { it('should register a valid tool handler', () => {
const validHandler: ToolHandler = { const validHandler: ToolHandler = {
definition: { definition: {
type: 'function', type: 'function',
@ -57,9 +56,11 @@ describe('ToolRegistry', () => {
name: 'test_tool', name: 'test_tool',
description: 'A test tool', description: 'A test tool',
parameters: { parameters: {
type: 'object', type: 'object' as const,
properties: {}, properties: {
required: [] input: { type: 'string', description: 'Input parameter' }
},
required: ['input']
} }
} }
}, },
@ -71,119 +72,59 @@ describe('ToolRegistry', () => {
expect(registry.getTool('test_tool')).toBe(validHandler); expect(registry.getTool('test_tool')).toBe(validHandler);
}); });
it('should reject null or undefined handler', () => { it('should handle registration of multiple tools', () => {
registry.registerTool(null as any); const tool1: ToolHandler = {
registry.registerTool(undefined as any);
expect(registry.getTools()).toHaveLength(0);
});
it('should reject handler without definition', () => {
const invalidHandler = {
execute: vi.fn()
} as any;
registry.registerTool(invalidHandler);
expect(registry.getTools()).toHaveLength(0);
});
it('should reject handler without function definition', () => {
const invalidHandler = {
definition: {
type: 'function'
},
execute: vi.fn()
} as any;
registry.registerTool(invalidHandler);
expect(registry.getTools()).toHaveLength(0);
});
it('should reject handler without function name', () => {
const invalidHandler = {
definition: { definition: {
type: 'function', type: 'function',
function: { function: {
description: 'Missing name' name: 'tool1',
} description: 'First tool',
},
execute: vi.fn()
} as any;
registry.registerTool(invalidHandler);
expect(registry.getTools()).toHaveLength(0);
});
it('should reject handler without execute method', () => {
const invalidHandler = {
definition: {
type: 'function',
function: {
name: 'test_tool',
description: 'Test tool'
}
}
} as any;
registry.registerTool(invalidHandler);
expect(registry.getTools()).toHaveLength(0);
});
it('should reject handler with non-function execute', () => {
const invalidHandler = {
definition: {
type: 'function',
function: {
name: 'test_tool',
description: 'Test tool'
}
},
execute: 'not a function'
} as any;
registry.registerTool(invalidHandler);
expect(registry.getTools()).toHaveLength(0);
});
});
describe('tool registration', () => {
it('should register a valid tool', () => {
const handler: ToolHandler = {
definition: {
type: 'function',
function: {
name: 'calculator',
description: 'Performs calculations',
parameters: { parameters: {
type: 'object', type: 'object' as const,
properties: { properties: {},
expression: { type: 'string' } required: []
},
required: ['expression']
} }
} }
}, },
execute: vi.fn().mockResolvedValue('42') execute: vi.fn()
}; };
registry.registerTool(handler); const tool2: ToolHandler = {
definition: {
type: 'function',
function: {
name: 'tool2',
description: 'Second tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
}
},
execute: vi.fn()
};
registry.registerTool(tool1);
registry.registerTool(tool2);
expect(registry.getTool('calculator')).toBe(handler); expect(registry.getTool('tool1')).toBe(tool1);
expect(registry.getTools()).toHaveLength(1); expect(registry.getTool('tool2')).toBe(tool2);
expect(registry.getAllTools()).toHaveLength(2);
}); });
it('should prevent duplicate tool registration', () => { it('should handle duplicate tool registration (overwrites)', () => {
const handler1: ToolHandler = { const handler1: ToolHandler = {
definition: { definition: {
type: 'function', type: 'function',
function: { function: {
name: 'duplicate_tool', name: 'duplicate_tool',
description: 'First version' description: 'First version',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
} }
}, },
execute: vi.fn() execute: vi.fn()
@ -194,7 +135,12 @@ describe('ToolRegistry', () => {
type: 'function', type: 'function',
function: { function: {
name: 'duplicate_tool', name: 'duplicate_tool',
description: 'Second version' description: 'Second version',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
} }
}, },
execute: vi.fn() execute: vi.fn()
@ -203,43 +149,41 @@ describe('ToolRegistry', () => {
registry.registerTool(handler1); registry.registerTool(handler1);
registry.registerTool(handler2); registry.registerTool(handler2);
// Should keep the first registration // Should have the second handler (overwrites)
expect(registry.getTool('duplicate_tool')).toBe(handler1); expect(registry.getTool('duplicate_tool')).toBe(handler2);
expect(registry.getTools()).toHaveLength(1); expect(registry.getAllTools()).toHaveLength(1);
});
it('should handle registration errors gracefully', () => {
const handlerWithError = {
definition: {
type: 'function',
function: {
name: 'error_tool',
description: 'This will cause an error'
}
},
execute: null
} as any;
// Should not throw
expect(() => registry.registerTool(handlerWithError)).not.toThrow();
expect(registry.getTool('error_tool')).toBeUndefined();
}); });
}); });
describe('tool retrieval', () => { describe('getTool', () => {
beforeEach(() => { beforeEach(() => {
const tools = [ const tools = [
{ {
name: 'tool1', name: 'tool1',
description: 'First tool' description: 'First tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
}, },
{ {
name: 'tool2', name: 'tool2',
description: 'Second tool' description: 'Second tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
}, },
{ {
name: 'tool3', name: 'tool3',
description: 'Third tool' description: 'Third tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
} }
]; ];
@ -255,256 +199,202 @@ describe('ToolRegistry', () => {
}); });
}); });
it('should retrieve a tool by name', () => { it('should return registered tool by name', () => {
const tool = registry.getTool('tool1'); const tool = registry.getTool('tool1');
expect(tool).toBeDefined(); expect(tool).toBeDefined();
expect(tool?.definition.function.name).toBe('tool1'); expect(tool?.definition.function.name).toBe('tool1');
}); });
it('should return undefined for non-existent tool', () => { it('should return undefined for non-existent tool', () => {
const tool = registry.getTool('non_existent'); const tool = registry.getTool('non_existent');
expect(tool).toBeUndefined(); expect(tool).toBeUndefined();
}); });
it('should get all registered tools', () => { it('should handle case-sensitive tool names', () => {
const tools = registry.getTools(); const tool = registry.getTool('Tool1'); // Different case
expect(tool).toBeUndefined();
});
});
describe('getAllTools', () => {
beforeEach(() => {
const tools = [
{
name: 'tool1',
description: 'First tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
},
{
name: 'tool2',
description: 'Second tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
},
{
name: 'tool3',
description: 'Third tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
}
];
tools.forEach(tool => {
const handler: ToolHandler = {
definition: {
type: 'function',
function: tool
},
execute: vi.fn()
};
registry.registerTool(handler);
});
});
it('should return all registered tools', () => {
const tools = registry.getAllTools();
expect(tools).toHaveLength(3); expect(tools).toHaveLength(3);
expect(tools.map(t => t.definition.function.name)).toEqual(['tool1', 'tool2', 'tool3']); expect(tools.map(t => t.definition.function.name)).toEqual(['tool1', 'tool2', 'tool3']);
}); });
it('should get tool definitions', () => { it('should return empty array when no tools registered', () => {
const definitions = registry.getToolDefinitions(); (registry as any).tools.clear();
const tools = registry.getAllTools();
expect(tools).toHaveLength(0);
});
});
describe('getAllToolDefinitions', () => {
beforeEach(() => {
const tools = [
{
name: 'tool1',
description: 'First tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
},
{
name: 'tool2',
description: 'Second tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
},
{
name: 'tool3',
description: 'Third tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
}
];
tools.forEach(tool => {
const handler: ToolHandler = {
definition: {
type: 'function',
function: tool
},
execute: vi.fn()
};
registry.registerTool(handler);
});
});
it('should return all tool definitions', () => {
const definitions = registry.getAllToolDefinitions();
expect(definitions).toHaveLength(3); expect(definitions).toHaveLength(3);
expect(definitions[0]).toEqual({ expect(definitions[0]).toEqual({
type: 'function', type: 'function',
function: { function: {
name: 'tool1', name: 'tool1',
description: 'First tool' description: 'First tool',
parameters: {
type: 'object' as const,
properties: {},
required: []
}
} }
}); });
}); });
it('should check if tool exists', () => { it('should return empty array when no tools registered', () => {
expect(registry.hasTool('tool1')).toBe(true); (registry as any).tools.clear();
expect(registry.hasTool('non_existent')).toBe(false);
});
});
describe('tool execution', () => {
let mockHandler: ToolHandler;
beforeEach(() => {
mockHandler = {
definition: {
type: 'function',
function: {
name: 'test_executor',
description: 'Test tool for execution',
parameters: {
type: 'object',
properties: {
input: { type: 'string' }
},
required: ['input']
}
}
},
execute: vi.fn().mockResolvedValue('execution result')
};
registry.registerTool(mockHandler);
});
it('should execute a tool with arguments', async () => {
const result = await registry.executeTool('test_executor', { input: 'test value' });
expect(result).toBe('execution result'); const definitions = registry.getAllToolDefinitions();
expect(mockHandler.execute).toHaveBeenCalledWith({ input: 'test value' }); expect(definitions).toHaveLength(0);
});
it('should throw error for non-existent tool', async () => {
await expect(
registry.executeTool('non_existent', {})
).rejects.toThrow('Tool non_existent not found');
});
it('should handle tool execution errors', async () => {
mockHandler.execute = vi.fn().mockRejectedValue(new Error('Execution failed'));
await expect(
registry.executeTool('test_executor', { input: 'test' })
).rejects.toThrow('Execution failed');
});
it('should execute tool without arguments', async () => {
const simpleHandler: ToolHandler = {
definition: {
type: 'function',
function: {
name: 'simple_tool',
description: 'Simple tool'
}
},
execute: vi.fn().mockResolvedValue('simple result')
};
registry.registerTool(simpleHandler);
const result = await registry.executeTool('simple_tool');
expect(result).toBe('simple result');
expect(simpleHandler.execute).toHaveBeenCalledWith(undefined);
});
});
describe('tool unregistration', () => {
beforeEach(() => {
const handler: ToolHandler = {
definition: {
type: 'function',
function: {
name: 'removable_tool',
description: 'A tool that can be removed'
}
},
execute: vi.fn()
};
registry.registerTool(handler);
});
it('should unregister a tool', () => {
expect(registry.hasTool('removable_tool')).toBe(true);
registry.unregisterTool('removable_tool');
expect(registry.hasTool('removable_tool')).toBe(false);
});
it('should handle unregistering non-existent tool', () => {
// Should not throw
expect(() => registry.unregisterTool('non_existent')).not.toThrow();
});
});
describe('registry clearing', () => {
beforeEach(() => {
// Register multiple tools
for (let i = 1; i <= 3; i++) {
const handler: ToolHandler = {
definition: {
type: 'function',
function: {
name: `tool${i}`,
description: `Tool ${i}`
}
},
execute: vi.fn()
};
registry.registerTool(handler);
}
});
it('should clear all tools', () => {
expect(registry.getTools()).toHaveLength(3);
registry.clear();
expect(registry.getTools()).toHaveLength(0);
});
it('should reset initialization flag on clear', () => {
(registry as any).initializationAttempted = true;
registry.clear();
expect((registry as any).initializationAttempted).toBe(false);
});
});
describe('initialization handling', () => {
it('should attempt initialization when registry is empty', () => {
const emptyRegistry = ToolRegistry.getInstance();
(emptyRegistry as any).tools.clear();
(emptyRegistry as any).initializationAttempted = false;
// Try to get tools which should trigger initialization attempt
emptyRegistry.getTools();
expect((emptyRegistry as any).initializationAttempted).toBe(true);
});
it('should not attempt initialization twice', () => {
const spy = vi.spyOn(registry as any, 'tryInitializeTools');
registry.getTools(); // First call
registry.getTools(); // Second call
expect(spy).toHaveBeenCalledTimes(1);
});
it('should not attempt initialization if tools exist', () => {
const handler: ToolHandler = {
definition: {
type: 'function',
function: {
name: 'existing_tool',
description: 'Already exists'
}
},
execute: vi.fn()
};
registry.registerTool(handler);
const spy = vi.spyOn(registry as any, 'tryInitializeTools');
registry.getTools();
expect(spy).not.toHaveBeenCalled();
}); });
}); });
describe('error handling', () => { describe('error handling', () => {
it('should handle validation errors gracefully', () => { it('should handle null/undefined tool handler gracefully', () => {
const problematicHandler = { // These should not crash the registry
definition: { expect(() => registry.registerTool(null as any)).not.toThrow();
type: 'function', expect(() => registry.registerTool(undefined as any)).not.toThrow();
function: {
name: 'problematic', // Registry should still work normally
description: 'This will cause validation issues' expect(registry.getAllTools()).toHaveLength(0);
}
},
execute: () => { throw new Error('Validation error'); }
} as any;
// Should not throw during registration
expect(() => registry.registerTool(problematicHandler)).not.toThrow();
}); });
it('should handle tool execution that throws synchronously', async () => { it('should handle malformed tool handler gracefully', () => {
const throwingHandler: ToolHandler = { const malformedHandler = {
// Missing definition
execute: vi.fn()
} as any;
expect(() => registry.registerTool(malformedHandler)).not.toThrow();
// Should not be registered
expect(registry.getAllTools()).toHaveLength(0);
});
});
describe('tool validation', () => {
it('should accept tool with proper structure', () => {
const validHandler: ToolHandler = {
definition: { definition: {
type: 'function', type: 'function',
function: { function: {
name: 'throwing_tool', name: 'calculator',
description: 'Throws an error' description: 'Performs calculations',
parameters: {
type: 'object' as const,
properties: {
expression: {
type: 'string',
description: 'The mathematical expression to evaluate'
}
},
required: ['expression']
}
} }
}, },
execute: vi.fn().mockImplementation(() => { execute: vi.fn().mockResolvedValue('42')
throw new Error('Synchronous error');
})
}; };
registry.registerTool(throwingHandler); registry.registerTool(validHandler);
await expect( expect(registry.getTool('calculator')).toBe(validHandler);
registry.executeTool('throwing_tool', {}) expect(registry.getAllTools()).toHaveLength(1);
).rejects.toThrow('Synchronous error');
}); });
}); });
}); });