Skip to main content
Glama
ErrorService.test.tsβ€’17.3 kB
/** * Test suite for ErrorService * * Tests error handling and suggestion functionality extracted from shared-handlers.ts * as part of Issue #489 Phase 3. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ErrorService } from '../../../src/services/ErrorService.js'; import { UniversalValidationError, ErrorType, } from '../../../src/handlers/tool-configs/universal/schemas.js'; import { EnhancedApiError } from '../../../src/errors/enhanced-api-errors.js'; // Mock the dependencies vi.mock('../../../src/handlers/tool-configs/universal/field-mapper.js', () => ({ validateResourceType: vi.fn(), getFieldSuggestions: vi.fn(), })); import { validateResourceType, getFieldSuggestions, } from '../../../src/handlers/tool-configs/universal/field-mapper.js'; describe('ErrorService', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('createUniversalError', () => { beforeEach(() => { // Set up default mock return for validateResourceType vi.mocked(validateResourceType).mockReturnValue({ valid: true, suggestion: undefined, }); }); it('should pass through UniversalValidationError unchanged', () => { const originalError = new UniversalValidationError( 'Test error', ErrorType.USER_ERROR ); const result = ErrorService.createUniversalError( 'create', 'companies', originalError ); expect(result).toBe(originalError); }); it('should pass through EnhancedApiError unchanged', () => { const originalError = new EnhancedApiError( 'Test error', 400, '/api/test', 'GET' ); const result = ErrorService.createUniversalError( 'update', 'people', originalError ); expect(result).toBe(originalError); }); it('should extract message from Error objects', () => { const originalError = new Error('Test error message'); const result = ErrorService.createUniversalError( 'search', 'tasks', originalError ); expect(result).toBeInstanceOf(UniversalValidationError); expect(result.message).toContain('Test error message'); expect(result.message).toContain( 'Universal search failed for resource type tasks' ); }); it('should extract message from objects with message property', () => { const originalError = { message: 'Object error message', status: 400 }; const result = ErrorService.createUniversalError( 'delete', 'deals', originalError ); expect(result).toBeInstanceOf(UniversalValidationError); expect(result.message).toContain('Object error message'); }); it('should handle string errors', () => { const originalError = 'String error message'; const result = ErrorService.createUniversalError( 'create', 'companies', originalError ); expect(result).toBeInstanceOf(UniversalValidationError); expect(result.message).toContain('String error message'); }); it('should handle unknown error types', () => { const originalError = 12345; const result = ErrorService.createUniversalError( 'update', 'people', originalError ); expect(result).toBeInstanceOf(UniversalValidationError); expect(result.message).toContain('Unknown error'); }); it('should classify USER_ERROR for not found messages', () => { const originalError = new Error('Record not found'); const result = ErrorService.createUniversalError( 'get', 'companies', originalError ) as UniversalValidationError; expect(result.errorType).toBe(ErrorType.USER_ERROR); }); it('should classify USER_ERROR for invalid messages', () => { const originalError = new Error('Invalid field name'); const result = ErrorService.createUniversalError( 'create', 'people', originalError ) as UniversalValidationError; expect(result.errorType).toBe(ErrorType.USER_ERROR); }); it('should classify USER_ERROR for required messages', () => { const originalError = new Error('Required field missing'); const result = ErrorService.createUniversalError( 'update', 'tasks', originalError ) as UniversalValidationError; expect(result.errorType).toBe(ErrorType.USER_ERROR); }); it('should classify USER_ERROR for 400 status codes', () => { const originalError = { message: 'Bad request', status: 400 }; const result = ErrorService.createUniversalError( 'create', 'deals', originalError ) as UniversalValidationError; expect(result.errorType).toBe(ErrorType.USER_ERROR); }); it('should classify API_ERROR for 500+ status codes', () => { const originalError = { message: 'Internal server error', status: 500 }; const result = ErrorService.createUniversalError( 'search', 'companies', originalError ) as UniversalValidationError; expect(result.errorType).toBe(ErrorType.API_ERROR); }); it('should classify API_ERROR for network errors', () => { const originalError = new Error('Network timeout occurred'); const result = ErrorService.createUniversalError( 'get', 'people', originalError ) as UniversalValidationError; expect(result.errorType).toBe(ErrorType.API_ERROR); }); it('should default to SYSTEM_ERROR for unclassified errors', () => { const originalError = new Error('Some random error'); const result = ErrorService.createUniversalError( 'create', 'tasks', originalError ) as UniversalValidationError; expect(result.errorType).toBe(ErrorType.SYSTEM_ERROR); }); it('should include operation suggestion in error details', () => { vi.mocked(validateResourceType).mockReturnValue({ valid: true, suggestion: undefined, }); // Use a specific error that will generate a suggestion const originalError = new Error('Rate limit exceeded'); const result = ErrorService.createUniversalError( 'create', 'companies', originalError ) as UniversalValidationError; expect(result.suggestion).toBeDefined(); expect(result.suggestion).toContain('wait a moment'); }); it('should include original error as cause', () => { const originalError = new Error('Original error'); const result = ErrorService.createUniversalError( 'update', 'people', originalError ) as UniversalValidationError; expect(result.cause).toBe(originalError); }); }); describe('getOperationSuggestion', () => { beforeEach(() => { vi.mocked(validateResourceType).mockReturnValue({ valid: true, suggestion: undefined, }); }); it('should return resource type suggestion for invalid resource', () => { vi.mocked(validateResourceType).mockReturnValue({ valid: false, suggestion: 'Use "companies" instead of "company"', }); const result = ErrorService.getOperationSuggestion( 'create', 'company', new Error('Invalid resource') ); expect(result).toBe('Use "companies" instead of "company"'); }); it('should provide date format suggestions for date parsing errors', () => { const error = new Error('Unable to parse date format'); const result = ErrorService.getOperationSuggestion( 'search', 'companies', error ); expect(result).toContain('relative dates like "last 7 days"'); expect(result).toContain('ISO format (YYYY-MM-DD)'); }); it('should provide date range suggestions', () => { const error = new Error('Invalid daterange specified'); const result = ErrorService.getOperationSuggestion( 'search', 'people', error ); expect(result).toContain('Date ranges support formats like'); expect(result).toContain('last 30 days'); }); it('should suggest simpler filters for unsupported filter combinations', () => { const error = new Error('Filter combination not supported'); const result = ErrorService.getOperationSuggestion( 'search', 'tasks', error ); expect(result).toContain('simpler filter'); expect(result).toContain('fetching all records'); }); it('should suggest batch size limits', () => { const error = new Error('Batch operation exceeds limit'); const result = ErrorService.getOperationSuggestion( 'create', 'companies', error ); expect(result).toContain('100 items at a time'); expect(result).toContain('smaller batches'); }); it('should suggest rate limit handling', () => { const error = new Error('Rate limit exceeded'); const result = ErrorService.getOperationSuggestion( 'search', 'people', error ); expect(result).toContain('wait a moment'); expect(result).toContain('reduce the frequency'); }); it('should provide deal-specific suggestions for company_id error', () => { const error = new Error( 'Cannot find attribute with slug/id "company_id"' ); const result = ErrorService.getOperationSuggestion( 'create', 'deals', error ); expect(result).toContain('associated_company'); expect(result).toContain('instead of "company_id"'); }); it('should provide deal-specific suggestions for deal_stage error', () => { const error = new Error('Cannot find deal_stage field'); const result = ErrorService.getOperationSuggestion( 'update', 'deals', error ); expect(result).toContain('Use "stage" instead of "deal_stage"'); }); it('should provide deal-specific suggestions for deal_value error', () => { const error = new Error('Unknown deal_value field'); const result = ErrorService.getOperationSuggestion( 'create', 'deals', error ); expect(result).toContain('Use "value" instead of "deal_value"'); }); it('should provide deal-specific suggestions for invalid value format', () => { const error = new Error( 'Invalid value was passed to attribute with slug "value"' ); const result = ErrorService.getOperationSuggestion( 'create', 'deals', error ); expect(result).toContain('simple number'); expect(result).toContain('9780'); }); it('should suggest field discovery for unknown attributes', () => { const error = new Error( 'Cannot find attribute with slug/id "unknown_field"' ); vi.mocked(getFieldSuggestions).mockReturnValue( 'Try using "name" or "email"' ); const result = ErrorService.getOperationSuggestion( 'create', 'companies', error ); expect(result).toBe('Try using "name" or "email"'); expect(getFieldSuggestions).toHaveBeenCalledWith( 'companies', 'unknown_field' ); }); it('should provide generic not found suggestions', () => { const error = new Error('Record not found'); const result = ErrorService.getOperationSuggestion( 'get', 'people', error ); expect(result).toContain('Verify that the people record exists'); expect(result).toContain('you have access to it'); }); it('should provide authentication suggestions for unauthorized errors', () => { const error = new Error('Unauthorized access'); const result = ErrorService.getOperationSuggestion( 'create', 'tasks', error ); expect(result).toContain('API permissions'); expect(result).toContain('authentication credentials'); }); it('should provide duplicate record suggestions for create operations', () => { const error = new Error('Duplicate record found'); const result = ErrorService.getOperationSuggestion( 'create', 'companies', error ); expect(result).toContain('may already exist'); expect(result).toContain('Try searching first'); }); it('should provide uniqueness constraint suggestions', () => { const error = new Error('Uniqueness constraint violation'); const result = ErrorService.getOperationSuggestion( 'create', 'people', error ); expect(result).toContain('unique values already exists'); expect(result).toContain('use different values'); }); it('should handle string errors', () => { const error = 'Rate limit exceeded'; const result = ErrorService.getOperationSuggestion( 'search', 'companies', error ); expect(result).toContain('wait a moment'); }); it('should handle object errors with message property', () => { const error = { message: 'Invalid date format' }; const result = ErrorService.getOperationSuggestion( 'search', 'people', error ); expect(result).toContain('relative dates'); }); it('should return undefined for unknown error patterns', () => { const error = new Error('Some completely unknown error'); const result = ErrorService.getOperationSuggestion( 'create', 'companies', error ); expect(result).toBeUndefined(); }); it('should provide deal-specific suggestions for various field errors', () => { const testCases = [ { error: 'description field not found', expected: 'do not have a "description" field', }, { error: 'expected_close_date invalid', expected: 'do not have a built-in close date field', }, { error: 'probability not supported', expected: 'do not have a built-in probability field', }, { error: 'source field missing', expected: 'do not have a built-in source field', }, { error: 'currency format invalid', expected: 'Currency is set automatically', }, { error: 'contact association failed', expected: 'Use "associated_people"', }, { error: 'notes field not found', expected: 'created separately using the notes API', }, { error: 'tags not supported', expected: 'do not have a built-in tags field', }, { error: 'deal_type unknown', expected: 'Deal types are not built-in' }, ]; testCases.forEach(({ error, expected }) => { const result = ErrorService.getOperationSuggestion( 'create', 'deals', new Error(error) ); expect(result).toContain(expected); }); }); it('should provide field suggestions for deal attribute errors', () => { const error = new Error('Cannot find attribute with slug/id "bad_field"'); const result = ErrorService.getOperationSuggestion( 'create', 'deals', error ); expect(result).toContain('Core fields: name, stage, value'); expect(result).toContain('discover-attributes tool'); }); it('should provide generic field suggestions for non-deal resources', () => { const error = new Error('Cannot find attribute with slug/id "bad_field"'); // Clear the mock return to get the fallback suggestion vi.mocked(getFieldSuggestions).mockReturnValue( 'Unable to provide suggestions for resource type companies' ); const result = ErrorService.getOperationSuggestion( 'create', 'companies', error ); expect(result).toContain('discover-attributes tool'); expect(result).toContain('available fields for companies'); }); }); describe('Edge Cases', () => { it('should handle null and undefined errors gracefully', () => { expect(() => { ErrorService.createUniversalError('create', 'companies', null); ErrorService.createUniversalError('create', 'companies', undefined); }).not.toThrow(); }); it('should handle empty error messages', () => { const result = ErrorService.getOperationSuggestion( 'create', 'companies', new Error('') ); // Should not crash and should return undefined for empty error expect(result).toBeUndefined(); }); it('should handle malformed error objects', () => { const malformedError = { notMessage: 'test', randomField: 123 }; const result = ErrorService.createUniversalError( 'create', 'companies', malformedError ); expect(result).toBeInstanceOf(UniversalValidationError); expect(result.message).toContain('Unknown 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/kesslerio/attio-mcp-server'

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