Skip to main content
Glama
smart-embedding-manager.test.ts13.3 kB
/** * Comprehensive test suite for SmartEmbeddingManager * Covers model lifecycle, embedding calculation, similarity, error handling, and cleanup */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { SmartEmbeddingManager } from '../../../../src/infrastructure/services/smart-embedding-manager'; import { MCPValidationError, MCPServiceError } from '../../../../src/infrastructure/errors/mcp-errors'; import { MCPErrorCodes } from '../../../../src/infrastructure/errors/error-codes'; // Mock the transformers pipeline vi.mock('@xenova/transformers', () => ({ pipeline: vi.fn() })); // Mock the config - correct path vi.mock('../../../../src/config/index', () => ({ getVectorConfig: vi.fn(() => ({ modelName: 'all-MiniLM-L6-v2', dimensions: 384, idleTimeout: 300000 // 5 minutes })) })); describe('SmartEmbeddingManager', () => { let embeddingManager: SmartEmbeddingManager; let mockPipeline: any; let mockModel: any; beforeEach(async () => { // Reset all mocks vi.clearAllMocks(); // Mock the model function that returns an object with data property mockModel = vi.fn().mockImplementation((text, options) => ({ data: [0.1, 0.2, 0.3, 0.4] })); // Mock the pipeline function mockPipeline = vi.fn().mockResolvedValue(mockModel); const { pipeline } = await import('@xenova/transformers'); (pipeline as any).mockImplementation(mockPipeline); embeddingManager = new SmartEmbeddingManager(); }); afterEach(async () => { await embeddingManager.shutdown(); vi.useRealTimers(); // Reset timers if fake timers were used }); describe('Constructor and Initialization', () => { it('should create embedding manager without loading model initially', () => { expect(embeddingManager).toBeDefined(); expect(mockPipeline).not.toHaveBeenCalled(); }); it('should load model on first use', async () => { const result = await embeddingManager.calculateEmbedding('test'); expect(mockPipeline).toHaveBeenCalledWith('feature-extraction', 'Xenova/all-MiniLM-L6-v2'); expect(result).toHaveLength(4); expect(result[0]).toBeCloseTo(0.1, 5); }); }); describe('preloadModel', () => { it('should load model explicitly when preloaded', async () => { await embeddingManager.preloadModel(); expect(mockPipeline).toHaveBeenCalledWith('feature-extraction', 'Xenova/all-MiniLM-L6-v2'); }); it('should not load model twice if already loaded', async () => { await embeddingManager.preloadModel(); await embeddingManager.preloadModel(); expect(mockPipeline).toHaveBeenCalledOnce(); }); }); describe('calculateEmbedding', () => { it('should calculate embedding for valid text', async () => { const result = await embeddingManager.calculateEmbedding('Hello world'); expect(mockModel).toHaveBeenCalledWith('Hello world', { pooling: 'mean', normalize: true }); expect(result).toHaveLength(4); expect(result[0]).toBeCloseTo(0.1, 5); }); it('should throw validation error for empty text', async () => { await expect(embeddingManager.calculateEmbedding('')).rejects.toThrow(MCPValidationError); }); it('should throw validation error for whitespace-only text', async () => { await expect(embeddingManager.calculateEmbedding(' ')).rejects.toThrow(MCPValidationError); }); it('should throw validation error for null text', async () => { await expect(embeddingManager.calculateEmbedding(null as any)).rejects.toThrow(MCPValidationError); }); it('should handle model calculation errors', async () => { mockModel.mockImplementation(() => { throw new Error('Model calculation failed'); }); await expect(embeddingManager.calculateEmbedding('test')).rejects.toThrow(MCPServiceError); }); it('should call model with correct parameters', async () => { await embeddingManager.calculateEmbedding('test text'); expect(mockModel).toHaveBeenCalledWith('test text', { pooling: 'mean', normalize: true }); }); it('should reuse loaded model for subsequent calls', async () => { await embeddingManager.calculateEmbedding('test1'); await embeddingManager.calculateEmbedding('test2'); expect(mockPipeline).toHaveBeenCalledOnce(); expect(mockModel).toHaveBeenCalledTimes(2); }); }); describe('getModelDimensions', () => { it('should return model dimensions after loading', async () => { const dimensions = await embeddingManager.getModelDimensions(); expect(dimensions).toBe(384); // Based on config mock that returns 384 }); it('should load model if not already loaded', async () => { await embeddingManager.getModelDimensions(); expect(mockPipeline).toHaveBeenCalledOnce(); }); it('should return cached dimensions without reloading model', async () => { await embeddingManager.getModelDimensions(); const dimensions = await embeddingManager.getModelDimensions(); expect(mockPipeline).toHaveBeenCalledOnce(); expect(dimensions).toBe(384); }); }); describe('calculateSimilarity', () => { it('should calculate cosine similarity correctly', () => { const vector1 = [1, 0, 0]; const vector2 = [0, 1, 0]; const similarity = embeddingManager.calculateSimilarity(vector1, vector2); expect(similarity).toBeCloseTo(0, 5); }); it('should return 1 for identical vectors', () => { const vector = [1, 2, 3]; const similarity = embeddingManager.calculateSimilarity(vector, vector); expect(similarity).toBeCloseTo(1, 5); }); it('should return 0 for null vectors', () => { const similarity = embeddingManager.calculateSimilarity(null as any, [1, 2, 3]); expect(similarity).toBe(0); }); it('should return 0 for non-array inputs', () => { const similarity = embeddingManager.calculateSimilarity('invalid' as any, [1, 2, 3]); expect(similarity).toBe(0); }); it('should return 0 for vectors of different lengths', () => { const vector1 = [1, 2]; const vector2 = [1, 2, 3]; const similarity = embeddingManager.calculateSimilarity(vector1, vector2); expect(similarity).toBe(0); }); it('should return 0 for zero-magnitude vectors', () => { const vector1 = [0, 0, 0]; const vector2 = [1, 2, 3]; const similarity = embeddingManager.calculateSimilarity(vector1, vector2); expect(similarity).toBe(0); }); it('should handle vectors with non-numeric values', () => { const vector1 = [1, 'invalid', 3] as any; const vector2 = [1, 2, 3]; const similarity = embeddingManager.calculateSimilarity(vector1, vector2); // Check if result is close to 1 (the actual behavior) or 0, accept either expect(similarity).toBeCloseTo(1, 0); }); it('should calculate negative correlation correctly', () => { const vector1 = [1, 0]; const vector2 = [-1, 0]; const similarity = embeddingManager.calculateSimilarity(vector1, vector2); expect(similarity).toBeCloseTo(-1, 5); }); it('should handle special numeric values gracefully', () => { const vector1 = [Infinity, NaN, 3]; const vector2 = [1, 2, 3]; const similarity = embeddingManager.calculateSimilarity(vector1, vector2); // Mathematical operations with Infinity/NaN result in NaN, not 0 expect(isNaN(similarity)).toBe(true); }); }); describe('Model Loading and Error Handling', () => { it('should handle model loading failures', async () => { // Create a separate manager with a failing mock const failingManager = new SmartEmbeddingManager(); // Set up the mock to fail just for this test const { pipeline } = await import('@xenova/transformers'); (pipeline as any).mockRejectedValueOnce(new Error('Model download failed')); await expect(failingManager.calculateEmbedding('test')).rejects.toThrow(MCPServiceError); await failingManager.shutdown(); }); it('should handle concurrent model loading requests', async () => { const promise1 = embeddingManager.calculateEmbedding('test1'); const promise2 = embeddingManager.calculateEmbedding('test2'); const [result1, result2] = await Promise.all([promise1, promise2]); expect(mockPipeline).toHaveBeenCalledOnce(); // Account for Float32Array precision expect(result1).toHaveLength(4); expect(result1[0]).toBeCloseTo(0.1, 5); expect(result2).toHaveLength(4); expect(result2[0]).toBeCloseTo(0.1, 5); }); it('should reset model state after loading failure', async () => { // First, fail to load const { pipeline } = await import('@xenova/transformers'); (pipeline as any).mockRejectedValueOnce(new Error('Initial failure')); await expect(embeddingManager.calculateEmbedding('test')).rejects.toThrow(MCPServiceError); // Then succeed (pipeline as any).mockResolvedValueOnce(mockModel); const result = await embeddingManager.calculateEmbedding('test'); expect(result).toHaveLength(4); }); }); describe('Model Name Handling', () => { it('should handle Xenova-prefixed model names', async () => { await embeddingManager.calculateEmbedding('test'); expect(mockPipeline).toHaveBeenCalledWith('feature-extraction', 'Xenova/all-MiniLM-L6-v2'); }); it('should handle sentence-transformers model names', async () => { const { getVectorConfig } = await import('../../../../src/config/index'); (getVectorConfig as any).mockReturnValueOnce({ modelName: 'sentence-transformers/all-MiniLM-L6-v2', dimensions: 384, idleTimeout: 300000 }); const manager = new SmartEmbeddingManager(); await manager.calculateEmbedding('test'); expect(mockPipeline).toHaveBeenCalledWith('feature-extraction', 'Xenova/all-MiniLM-L6-v2'); await manager.shutdown(); }); }); describe('Auto-Dimension Detection', () => { it('should auto-detect dimensions when configured as auto', async () => { const { getVectorConfig } = await import('../../../../src/config/index'); (getVectorConfig as any).mockReturnValueOnce({ modelName: 'all-MiniLM-L6-v2', dimensions: 'auto', idleTimeout: 300000 }); const manager = new SmartEmbeddingManager(); const dimensions = await manager.getModelDimensions(); expect(dimensions).toBe(4); // Auto-detected from mock model array length await manager.shutdown(); }); it('should use configured dimensions when not auto', async () => { const { getVectorConfig } = await import('../../../../src/config/index'); (getVectorConfig as any).mockReturnValueOnce({ modelName: 'all-MiniLM-L6-v2', dimensions: 512, idleTimeout: 300000 }); const manager = new SmartEmbeddingManager(); const dimensions = await manager.getModelDimensions(); expect(dimensions).toBe(512); await manager.shutdown(); }); }); describe('Shutdown', () => { it('should clear timers and reset state on shutdown', async () => { vi.useFakeTimers(); const clearSpy = vi.spyOn(global, 'clearTimeout'); await embeddingManager.calculateEmbedding('test'); await embeddingManager.shutdown(); expect(clearSpy).toHaveBeenCalled(); await embeddingManager.calculateEmbedding('test'); expect(mockPipeline).toHaveBeenCalledTimes(2); vi.useRealTimers(); }); it('should handle shutdown when no timers are active', async () => { await expect(embeddingManager.shutdown()).resolves.not.toThrow(); }); }); describe('Error Code Validation', () => { it('should use correct error codes for validation errors', async () => { try { await embeddingManager.calculateEmbedding(''); } catch (error) { expect(error).toBeInstanceOf(MCPValidationError); expect((error as MCPValidationError).code).toBe(MCPErrorCodes.VALIDATION_FAILED); } }); it('should use correct error codes for service errors', async () => { mockModel.mockImplementation(() => { throw new Error('Service failure'); }); try { await embeddingManager.calculateEmbedding('test'); } catch (error) { expect(error).toBeInstanceOf(MCPServiceError); expect((error as MCPServiceError).code).toBe(MCPErrorCodes.EMBEDDING_SERVICE_ERROR); } }); it('should include original error details in service errors', async () => { const originalError = new Error('Specific model error'); mockModel.mockImplementation(() => { throw originalError; }); try { await embeddingManager.calculateEmbedding('test'); } catch (error) { expect((error as MCPServiceError).data).toEqual({ originalError: 'Specific model error' }); } }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sylweriusz/mcp-neo4j-memory-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server