Skip to main content
Glama

n8n-MCP

by 88-888
operation-similarity-service-comprehensive.test.tsโ€ข32 kB
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { OperationSimilarityService } from '@/services/operation-similarity-service'; import { NodeRepository } from '@/database/node-repository'; import { ValidationServiceError } from '@/errors/validation-service-error'; import { logger } from '@/utils/logger'; // Mock the logger to test error handling paths vi.mock('@/utils/logger', () => ({ logger: { warn: vi.fn(), error: vi.fn() } })); describe('OperationSimilarityService - Comprehensive Coverage', () => { let service: OperationSimilarityService; let mockRepository: any; beforeEach(() => { mockRepository = { getNode: vi.fn() }; service = new OperationSimilarityService(mockRepository); vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); describe('constructor and initialization', () => { it('should initialize with common patterns', () => { const patterns = (service as any).commonPatterns; expect(patterns).toBeDefined(); expect(patterns.has('googleDrive')).toBe(true); expect(patterns.has('slack')).toBe(true); expect(patterns.has('database')).toBe(true); expect(patterns.has('httpRequest')).toBe(true); expect(patterns.has('generic')).toBe(true); }); it('should initialize empty caches', () => { const operationCache = (service as any).operationCache; const suggestionCache = (service as any).suggestionCache; expect(operationCache.size).toBe(0); expect(suggestionCache.size).toBe(0); }); }); describe('cache cleanup mechanisms', () => { it('should clean up expired operation cache entries', () => { const now = Date.now(); const expiredTimestamp = now - (6 * 60 * 1000); // 6 minutes ago const validTimestamp = now - (2 * 60 * 1000); // 2 minutes ago const operationCache = (service as any).operationCache; operationCache.set('expired-node', { operations: [], timestamp: expiredTimestamp }); operationCache.set('valid-node', { operations: [], timestamp: validTimestamp }); (service as any).cleanupExpiredEntries(); expect(operationCache.has('expired-node')).toBe(false); expect(operationCache.has('valid-node')).toBe(true); }); it('should limit suggestion cache size to 50 entries when over 100', () => { const suggestionCache = (service as any).suggestionCache; // Fill cache with 110 entries for (let i = 0; i < 110; i++) { suggestionCache.set(`key-${i}`, []); } expect(suggestionCache.size).toBe(110); (service as any).cleanupExpiredEntries(); expect(suggestionCache.size).toBe(50); // Should keep the last 50 entries expect(suggestionCache.has('key-109')).toBe(true); expect(suggestionCache.has('key-59')).toBe(false); }); it('should trigger random cleanup during findSimilarOperations', () => { const cleanupSpy = vi.spyOn(service as any, 'cleanupExpiredEntries'); mockRepository.getNode.mockReturnValue({ operations: [{ operation: 'test', name: 'Test' }], properties: [] }); // Mock Math.random to always trigger cleanup const originalRandom = Math.random; Math.random = vi.fn(() => 0.05); // Less than 0.1 service.findSimilarOperations('nodes-base.test', 'invalid'); expect(cleanupSpy).toHaveBeenCalled(); Math.random = originalRandom; }); }); describe('getOperationValue edge cases', () => { it('should handle string operations', () => { const getValue = (service as any).getOperationValue.bind(service); expect(getValue('test-operation')).toBe('test-operation'); }); it('should handle object operations with operation property', () => { const getValue = (service as any).getOperationValue.bind(service); expect(getValue({ operation: 'send', name: 'Send Message' })).toBe('send'); }); it('should handle object operations with value property', () => { const getValue = (service as any).getOperationValue.bind(service); expect(getValue({ value: 'create', displayName: 'Create' })).toBe('create'); }); it('should handle object operations without operation or value properties', () => { const getValue = (service as any).getOperationValue.bind(service); expect(getValue({ name: 'Some Operation' })).toBe(''); }); it('should handle null and undefined operations', () => { const getValue = (service as any).getOperationValue.bind(service); expect(getValue(null)).toBe(''); expect(getValue(undefined)).toBe(''); }); it('should handle primitive types', () => { const getValue = (service as any).getOperationValue.bind(service); expect(getValue(123)).toBe(''); expect(getValue(true)).toBe(''); }); }); describe('getResourceValue edge cases', () => { it('should handle string resources', () => { const getValue = (service as any).getResourceValue.bind(service); expect(getValue('test-resource')).toBe('test-resource'); }); it('should handle object resources with value property', () => { const getValue = (service as any).getResourceValue.bind(service); expect(getValue({ value: 'message', name: 'Message' })).toBe('message'); }); it('should handle object resources without value property', () => { const getValue = (service as any).getResourceValue.bind(service); expect(getValue({ name: 'Resource' })).toBe(''); }); it('should handle null and undefined resources', () => { const getValue = (service as any).getResourceValue.bind(service); expect(getValue(null)).toBe(''); expect(getValue(undefined)).toBe(''); }); }); describe('getNodeOperations error handling', () => { it('should return empty array when node not found', () => { mockRepository.getNode.mockReturnValue(null); const operations = (service as any).getNodeOperations('nodes-base.nonexistent'); expect(operations).toEqual([]); }); it('should handle JSON parsing errors and throw ValidationServiceError', () => { mockRepository.getNode.mockReturnValue({ operations: '{invalid json}', // Malformed JSON string properties: [] }); expect(() => { (service as any).getNodeOperations('nodes-base.broken'); }).toThrow(ValidationServiceError); expect(logger.error).toHaveBeenCalled(); }); it('should handle generic errors in operations processing', () => { // Mock repository to throw an error when getting node mockRepository.getNode.mockImplementation(() => { throw new Error('Generic error'); }); // The public API should handle the error gracefully const result = service.findSimilarOperations('nodes-base.error', 'invalidOp'); expect(result).toEqual([]); }); it('should handle errors in properties processing', () => { // Mock repository to return null to trigger error path mockRepository.getNode.mockReturnValue(null); const result = service.findSimilarOperations('nodes-base.props-error', 'invalidOp'); expect(result).toEqual([]); }); it('should parse string operations correctly', () => { mockRepository.getNode.mockReturnValue({ operations: JSON.stringify([ { operation: 'send', name: 'Send Message' }, { operation: 'get', name: 'Get Message' } ]), properties: [] }); const operations = (service as any).getNodeOperations('nodes-base.string-ops'); expect(operations).toHaveLength(2); expect(operations[0].operation).toBe('send'); }); it('should handle array operations directly', () => { mockRepository.getNode.mockReturnValue({ operations: [ { operation: 'create', name: 'Create Item' }, { operation: 'delete', name: 'Delete Item' } ], properties: [] }); const operations = (service as any).getNodeOperations('nodes-base.array-ops'); expect(operations).toHaveLength(2); expect(operations[1].operation).toBe('delete'); }); it('should flatten object operations', () => { mockRepository.getNode.mockReturnValue({ operations: { message: [{ operation: 'send' }], channel: [{ operation: 'create' }] }, properties: [] }); const operations = (service as any).getNodeOperations('nodes-base.object-ops'); expect(operations).toHaveLength(2); }); it('should extract operations from properties with resource filtering', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', displayOptions: { show: { resource: ['message'] } }, options: [ { value: 'send', name: 'Send Message' }, { value: 'update', name: 'Update Message' } ] } ] }); // Test through public API instead of private method const messageOpsSuggestions = service.findSimilarOperations('nodes-base.slack', 'messageOp', 'message'); const allOpsSuggestions = service.findSimilarOperations('nodes-base.slack', 'nonExistentOp'); // Should find similarity-based suggestions, not exact match expect(messageOpsSuggestions.length).toBeGreaterThanOrEqual(0); expect(allOpsSuggestions.length).toBeGreaterThanOrEqual(0); }); it('should filter operations by resource correctly', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', displayOptions: { show: { resource: ['message'] } }, options: [ { value: 'send', name: 'Send Message' } ] }, { name: 'operation', displayOptions: { show: { resource: ['channel'] } }, options: [ { value: 'create', name: 'Create Channel' } ] } ] }); // Test resource filtering through public API with similar operations const messageSuggestions = service.findSimilarOperations('nodes-base.slack', 'sendMsg', 'message'); const channelSuggestions = service.findSimilarOperations('nodes-base.slack', 'createChannel', 'channel'); const wrongResourceSuggestions = service.findSimilarOperations('nodes-base.slack', 'sendMsg', 'nonexistent'); // Should find send operation when resource is message const sendSuggestion = messageSuggestions.find(s => s.value === 'send'); expect(sendSuggestion).toBeDefined(); expect(sendSuggestion?.resource).toBe('message'); // Should find create operation when resource is channel const createSuggestion = channelSuggestions.find(s => s.value === 'create'); expect(createSuggestion).toBeDefined(); expect(createSuggestion?.resource).toBe('channel'); // Should find few or no operations for wrong resource // The resource filtering should significantly reduce suggestions expect(wrongResourceSuggestions.length).toBeLessThanOrEqual(1); // Allow some fuzzy matching }); it('should handle array resource filters', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', displayOptions: { show: { resource: ['message', 'channel'] // Array format } }, options: [ { value: 'list', name: 'List Items' } ] } ] }); // Test array resource filtering through public API const messageSuggestions = service.findSimilarOperations('nodes-base.multi', 'listItems', 'message'); const channelSuggestions = service.findSimilarOperations('nodes-base.multi', 'listItems', 'channel'); const otherSuggestions = service.findSimilarOperations('nodes-base.multi', 'listItems', 'other'); // Should find list operation for both message and channel resources const messageListSuggestion = messageSuggestions.find(s => s.value === 'list'); const channelListSuggestion = channelSuggestions.find(s => s.value === 'list'); expect(messageListSuggestion).toBeDefined(); expect(channelListSuggestion).toBeDefined(); // Should find few or no operations for wrong resource expect(otherSuggestions.length).toBeLessThanOrEqual(1); // Allow some fuzzy matching }); }); describe('getNodePatterns', () => { it('should return Google Drive patterns for googleDrive nodes', () => { const patterns = (service as any).getNodePatterns('nodes-base.googleDrive'); const hasGoogleDrivePattern = patterns.some((p: any) => p.pattern === 'listFiles'); const hasGenericPattern = patterns.some((p: any) => p.pattern === 'list'); expect(hasGoogleDrivePattern).toBe(true); expect(hasGenericPattern).toBe(true); }); it('should return Slack patterns for slack nodes', () => { const patterns = (service as any).getNodePatterns('nodes-base.slack'); const hasSlackPattern = patterns.some((p: any) => p.pattern === 'sendMessage'); expect(hasSlackPattern).toBe(true); }); it('should return database patterns for database nodes', () => { const postgresPatterns = (service as any).getNodePatterns('nodes-base.postgres'); const mysqlPatterns = (service as any).getNodePatterns('nodes-base.mysql'); const mongoPatterns = (service as any).getNodePatterns('nodes-base.mongodb'); expect(postgresPatterns.some((p: any) => p.pattern === 'selectData')).toBe(true); expect(mysqlPatterns.some((p: any) => p.pattern === 'insertData')).toBe(true); expect(mongoPatterns.some((p: any) => p.pattern === 'updateData')).toBe(true); }); it('should return HTTP patterns for httpRequest nodes', () => { const patterns = (service as any).getNodePatterns('nodes-base.httpRequest'); const hasHttpPattern = patterns.some((p: any) => p.pattern === 'fetch'); expect(hasHttpPattern).toBe(true); }); it('should always include generic patterns', () => { const patterns = (service as any).getNodePatterns('nodes-base.unknown'); const hasGenericPattern = patterns.some((p: any) => p.pattern === 'list'); expect(hasGenericPattern).toBe(true); }); }); describe('similarity calculation', () => { describe('calculateSimilarity', () => { it('should return 1.0 for exact matches', () => { const similarity = (service as any).calculateSimilarity('send', 'send'); expect(similarity).toBe(1.0); }); it('should return high confidence for substring matches', () => { const similarity = (service as any).calculateSimilarity('send', 'sendMessage'); expect(similarity).toBeGreaterThanOrEqual(0.7); }); it('should boost confidence for single character typos in short words', () => { const similarity = (service as any).calculateSimilarity('send', 'senc'); // Single character substitution expect(similarity).toBeGreaterThanOrEqual(0.75); }); it('should boost confidence for transpositions in short words', () => { const similarity = (service as any).calculateSimilarity('sedn', 'send'); expect(similarity).toBeGreaterThanOrEqual(0.72); }); it('should boost similarity for common variations', () => { const similarity = (service as any).calculateSimilarity('sendmessage', 'send'); // Base similarity for substring match is 0.7, with boost should be ~0.9 // But if boost logic has issues, just check it's reasonable expect(similarity).toBeGreaterThanOrEqual(0.7); // At least base similarity }); it('should handle case insensitive matching', () => { const similarity = (service as any).calculateSimilarity('SEND', 'send'); expect(similarity).toBe(1.0); }); }); describe('levenshteinDistance', () => { it('should calculate distance 0 for identical strings', () => { const distance = (service as any).levenshteinDistance('send', 'send'); expect(distance).toBe(0); }); it('should calculate distance for single character operations', () => { const distance = (service as any).levenshteinDistance('send', 'sned'); expect(distance).toBe(2); // transposition }); it('should calculate distance for insertions', () => { const distance = (service as any).levenshteinDistance('send', 'sends'); expect(distance).toBe(1); }); it('should calculate distance for deletions', () => { const distance = (service as any).levenshteinDistance('sends', 'send'); expect(distance).toBe(1); }); it('should calculate distance for substitutions', () => { const distance = (service as any).levenshteinDistance('send', 'tend'); expect(distance).toBe(1); }); it('should handle empty strings', () => { const distance1 = (service as any).levenshteinDistance('', 'send'); const distance2 = (service as any).levenshteinDistance('send', ''); expect(distance1).toBe(4); expect(distance2).toBe(4); }); }); }); describe('areCommonVariations', () => { it('should detect common prefix variations', () => { const areCommon = (service as any).areCommonVariations.bind(service); expect(areCommon('getmessage', 'message')).toBe(true); expect(areCommon('senddata', 'data')).toBe(true); expect(areCommon('createitem', 'item')).toBe(true); }); it('should detect common suffix variations', () => { const areCommon = (service as any).areCommonVariations.bind(service); expect(areCommon('uploadfile', 'upload')).toBe(true); expect(areCommon('savedata', 'save')).toBe(true); expect(areCommon('sendmessage', 'send')).toBe(true); }); it('should handle small differences after prefix/suffix removal', () => { const areCommon = (service as any).areCommonVariations.bind(service); expect(areCommon('getmessages', 'message')).toBe(true); // get + messages vs message expect(areCommon('createitems', 'item')).toBe(true); // create + items vs item }); it('should return false for unrelated operations', () => { const areCommon = (service as any).areCommonVariations.bind(service); expect(areCommon('send', 'delete')).toBe(false); expect(areCommon('upload', 'search')).toBe(false); }); it('should handle edge cases', () => { const areCommon = (service as any).areCommonVariations.bind(service); expect(areCommon('', 'send')).toBe(false); expect(areCommon('send', '')).toBe(false); expect(areCommon('get', 'get')).toBe(false); // Same string, not variation }); }); describe('getSimilarityReason', () => { it('should return "Almost exact match" for very high confidence', () => { const reason = (service as any).getSimilarityReason(0.96, 'sned', 'send'); expect(reason).toBe('Almost exact match - likely a typo'); }); it('should return "Very similar" for high confidence', () => { const reason = (service as any).getSimilarityReason(0.85, 'sendMsg', 'send'); expect(reason).toBe('Very similar - common variation'); }); it('should return "Similar operation" for medium confidence', () => { const reason = (service as any).getSimilarityReason(0.65, 'create', 'update'); expect(reason).toBe('Similar operation'); }); it('should return "Partial match" for substring matches', () => { const reason = (service as any).getSimilarityReason(0.5, 'sendMessage', 'send'); expect(reason).toBe('Partial match'); }); it('should return "Possibly related operation" for low confidence', () => { const reason = (service as any).getSimilarityReason(0.4, 'xyz', 'send'); expect(reason).toBe('Possibly related operation'); }); }); describe('findSimilarOperations comprehensive scenarios', () => { it('should return empty array for non-existent node', () => { mockRepository.getNode.mockReturnValue(null); const suggestions = service.findSimilarOperations('nodes-base.nonexistent', 'operation'); expect(suggestions).toEqual([]); }); it('should return empty array for exact matches', () => { mockRepository.getNode.mockReturnValue({ operations: [{ operation: 'send', name: 'Send' }], properties: [] }); const suggestions = service.findSimilarOperations('nodes-base.test', 'send'); expect(suggestions).toEqual([]); }); it('should find pattern matches first', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', options: [ { value: 'search', name: 'Search' } ] } ] }); const suggestions = service.findSimilarOperations('nodes-base.googleDrive', 'listFiles'); expect(suggestions.length).toBeGreaterThan(0); const searchSuggestion = suggestions.find(s => s.value === 'search'); expect(searchSuggestion).toBeDefined(); expect(searchSuggestion!.confidence).toBe(0.85); }); it('should not suggest pattern matches if target operation doesn\'t exist', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', options: [ { value: 'someOtherOperation', name: 'Other Operation' } ] } ] }); const suggestions = service.findSimilarOperations('nodes-base.googleDrive', 'listFiles'); // Pattern suggests 'search' but it doesn't exist in the node const searchSuggestion = suggestions.find(s => s.value === 'search'); expect(searchSuggestion).toBeUndefined(); }); it('should calculate similarity for valid operations', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', options: [ { value: 'send', name: 'Send Message' }, { value: 'get', name: 'Get Message' }, { value: 'delete', name: 'Delete Message' } ] } ] }); const suggestions = service.findSimilarOperations('nodes-base.test', 'sned'); expect(suggestions.length).toBeGreaterThan(0); const sendSuggestion = suggestions.find(s => s.value === 'send'); expect(sendSuggestion).toBeDefined(); expect(sendSuggestion!.confidence).toBeGreaterThan(0.7); }); it('should include operation description when available', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', options: [ { value: 'send', name: 'Send Message', description: 'Send a message to a channel' } ] } ] }); const suggestions = service.findSimilarOperations('nodes-base.test', 'sned'); const sendSuggestion = suggestions.find(s => s.value === 'send'); expect(sendSuggestion!.description).toBe('Send a message to a channel'); }); it('should include resource information when specified', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', displayOptions: { show: { resource: ['message'] } }, options: [ { value: 'send', name: 'Send Message' } ] } ] }); const suggestions = service.findSimilarOperations('nodes-base.test', 'sned', 'message'); const sendSuggestion = suggestions.find(s => s.value === 'send'); expect(sendSuggestion!.resource).toBe('message'); }); it('should deduplicate suggestions from different sources', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', options: [ { value: 'send', name: 'Send' } ] } ] }); // This should find both pattern match and similarity match for the same operation const suggestions = service.findSimilarOperations('nodes-base.slack', 'sendMessage'); const sendCount = suggestions.filter(s => s.value === 'send').length; expect(sendCount).toBe(1); // Should be deduplicated }); it('should limit suggestions to maxSuggestions parameter', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', options: [ { value: 'operation1', name: 'Operation 1' }, { value: 'operation2', name: 'Operation 2' }, { value: 'operation3', name: 'Operation 3' }, { value: 'operation4', name: 'Operation 4' }, { value: 'operation5', name: 'Operation 5' }, { value: 'operation6', name: 'Operation 6' } ] } ] }); const suggestions = service.findSimilarOperations('nodes-base.test', 'operatio', undefined, 3); expect(suggestions.length).toBeLessThanOrEqual(3); }); it('should sort suggestions by confidence descending', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', options: [ { value: 'send', name: 'Send' }, { value: 'senda', name: 'Senda' }, { value: 'sending', name: 'Sending' } ] } ] }); const suggestions = service.findSimilarOperations('nodes-base.test', 'sned'); // Should be sorted by confidence for (let i = 0; i < suggestions.length - 1; i++) { expect(suggestions[i].confidence).toBeGreaterThanOrEqual(suggestions[i + 1].confidence); } }); it('should use cached results when available', () => { const suggestionCache = (service as any).suggestionCache; const cachedSuggestions = [{ value: 'cached', confidence: 0.9, reason: 'Cached' }]; suggestionCache.set('nodes-base.test:invalid:', cachedSuggestions); const suggestions = service.findSimilarOperations('nodes-base.test', 'invalid'); expect(suggestions).toEqual(cachedSuggestions); expect(mockRepository.getNode).not.toHaveBeenCalled(); }); it('should cache results after calculation', () => { mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', options: [{ value: 'test', name: 'Test' }] } ] }); const suggestions1 = service.findSimilarOperations('nodes-base.test', 'invalid'); const suggestions2 = service.findSimilarOperations('nodes-base.test', 'invalid'); expect(suggestions1).toEqual(suggestions2); // The suggestion cache should prevent any calls on the second invocation // But the implementation calls getNode during the first call to process operations // Since no exact cache match exists at the suggestion level initially, // we expect at least 1 call, but not more due to suggestion caching // Due to both suggestion cache and operation cache, there might be multiple calls // during the first invocation (findSimilarOperations calls getNode, then getNodeOperations also calls getNode) // But the second call to findSimilarOperations should be fully cached at suggestion level expect(mockRepository.getNode).toHaveBeenCalledTimes(2); // Called twice during first invocation }); }); describe('cache behavior edge cases', () => { it('should trigger getNodeOperations cache cleanup randomly', () => { const originalRandom = Math.random; Math.random = vi.fn(() => 0.02); // Less than 0.05 const cleanupSpy = vi.spyOn(service as any, 'cleanupExpiredEntries'); mockRepository.getNode.mockReturnValue({ operations: [], properties: [] }); (service as any).getNodeOperations('nodes-base.test'); expect(cleanupSpy).toHaveBeenCalled(); Math.random = originalRandom; }); it('should use cached operation data when available and fresh', () => { const operationCache = (service as any).operationCache; const testOperations = [{ operation: 'cached', name: 'Cached Operation' }]; operationCache.set('nodes-base.test:all', { operations: testOperations, timestamp: Date.now() - 1000 // 1 second ago, fresh }); const operations = (service as any).getNodeOperations('nodes-base.test'); expect(operations).toEqual(testOperations); expect(mockRepository.getNode).not.toHaveBeenCalled(); }); it('should refresh expired operation cache data', () => { const operationCache = (service as any).operationCache; const oldOperations = [{ operation: 'old', name: 'Old Operation' }]; const newOperations = [{ value: 'new', name: 'New Operation' }]; // Set expired cache entry operationCache.set('nodes-base.test:all', { operations: oldOperations, timestamp: Date.now() - (6 * 60 * 1000) // 6 minutes ago, expired }); mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', options: newOperations } ] }); const operations = (service as any).getNodeOperations('nodes-base.test'); expect(mockRepository.getNode).toHaveBeenCalled(); expect(operations[0].operation).toBe('new'); }); it('should handle resource-specific caching', () => { const operationCache = (service as any).operationCache; mockRepository.getNode.mockReturnValue({ operations: [], properties: [ { name: 'operation', displayOptions: { show: { resource: ['message'] } }, options: [{ value: 'send', name: 'Send' }] } ] }); // First call should cache const messageOps1 = (service as any).getNodeOperations('nodes-base.test', 'message'); expect(operationCache.has('nodes-base.test:message')).toBe(true); // Second call should use cache const messageOps2 = (service as any).getNodeOperations('nodes-base.test', 'message'); expect(messageOps1).toEqual(messageOps2); // Different resource should have separate cache const allOps = (service as any).getNodeOperations('nodes-base.test'); expect(operationCache.has('nodes-base.test:all')).toBe(true); }); }); describe('clearCache', () => { it('should clear both operation and suggestion caches', () => { const operationCache = (service as any).operationCache; const suggestionCache = (service as any).suggestionCache; // Add some data to caches operationCache.set('test', { operations: [], timestamp: Date.now() }); suggestionCache.set('test', []); expect(operationCache.size).toBe(1); expect(suggestionCache.size).toBe(1); service.clearCache(); expect(operationCache.size).toBe(0); expect(suggestionCache.size).toBe(0); }); }); });

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/88-888/n8n-mcp'

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