/**
* Tests for error handler
*
* The error handler converts various types of errors into structured responses.
* This tests all the different error types and edge cases.
*/
import { describe, it, expect } from 'vitest';
import {
handleToolError,
createSuccessResponse,
} from '../../../src/utils/error-handler.js';
import {
ConfigurationError,
ValidationError,
ErrorType,
} from '../../../src/utils/errors.js';
describe('createSuccessResponse', () => {
it('should create a success response with data', () => {
const data = { id: '123', name: 'Test Company' };
const result = createSuccessResponse(data);
// Parse the JSON response
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data).toEqual(data);
});
it('should format JSON with 2-space indentation', () => {
const result = createSuccessResponse({ test: true });
// Check that it's pretty-printed JSON
expect(result.content[0].text).toContain('\n');
expect(result.content[0].text).toContain(' '); // 2 spaces
});
});
describe('handleToolError - Custom Errors', () => {
it('should handle ConfigurationError', () => {
const error = new ConfigurationError('ATTIO_API_KEY not found');
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe(ErrorType.VALIDATION);
expect(parsed.error.message).toBe('ATTIO_API_KEY not found');
expect(parsed.error.suggestions).toContain('Check environment variables');
expect(parsed.error.retryable).toBe(false);
});
it('should handle ValidationError with field', () => {
const error = new ValidationError('Name is required', 'name');
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe(ErrorType.VALIDATION);
expect(parsed.error.message).toBe('Name is required');
expect(parsed.error.field).toBe('name');
expect(parsed.error.suggestions).toContain('Provide a valid value for name');
});
it('should handle ValidationError without field', () => {
const error = new ValidationError('Invalid input');
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.field).toBeUndefined();
expect(parsed.error.suggestions.length).toBeGreaterThan(0);
});
});
describe('handleToolError - Attio API Errors', () => {
it('should handle 404 Not Found', () => {
const error = {
statusCode: 404,
message: 'Record not found',
response: { message: 'Company not found' },
};
const result = handleToolError(error, 'get_company');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.NOT_FOUND);
expect(parsed.error.http_status).toBe(404);
expect(parsed.error.message).toBe('Company not found');
expect(parsed.error.suggestions).toContain('Verify the record_id is correct');
expect(parsed.error.retryable).toBe(false);
});
it('should handle 401 Authentication', () => {
const error = {
statusCode: 401,
message: 'Unauthorized',
};
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.AUTHENTICATION);
expect(parsed.error.http_status).toBe(401);
expect(parsed.error.suggestions).toContain('Verify ATTIO_API_KEY is correct');
expect(parsed.error.retryable).toBe(false);
});
it('should handle 403 Permission Denied', () => {
const error = {
statusCode: 403,
message: 'Forbidden',
};
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.PERMISSION);
expect(parsed.error.http_status).toBe(403);
expect(parsed.error.suggestions).toContain('API key lacks required permissions');
expect(parsed.error.retryable).toBe(false);
});
it('should handle 429 Rate Limit with retry-after', () => {
const error = {
statusCode: 429,
message: 'Too many requests',
response: {
retry_after: 120,
},
};
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.RATE_LIMIT);
expect(parsed.error.http_status).toBe(429);
expect(parsed.error.retry_after).toBe(120);
expect(parsed.error.suggestions).toContain('Wait 120 seconds before retrying');
expect(parsed.error.retryable).toBe(true); // Rate limits ARE retryable
});
it('should handle 500 Server Error', () => {
const error = {
statusCode: 500,
message: 'Internal server error',
};
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.SERVER_ERROR);
expect(parsed.error.http_status).toBe(500);
expect(parsed.error.suggestions).toContain('Retry after a short delay');
expect(parsed.error.retryable).toBe(true); // Server errors ARE retryable
});
it('should handle 503 Service Unavailable', () => {
const error = {
statusCode: 503,
message: 'Service unavailable',
};
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.SERVER_ERROR);
expect(parsed.error.http_status).toBe(503);
expect(parsed.error.retryable).toBe(true);
});
it('should handle uniqueness constraint conflicts', () => {
const error = {
statusCode: 400,
message:
'uniqueness constraint violated: The value "test@example.com" provided for attribute with slug "email"',
};
const result = handleToolError(error, 'create_person');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.CONFLICT);
expect(parsed.error.http_status).toBe(409);
expect(parsed.error.field).toBe('email');
expect(parsed.error.message).toBe('A record with this value already exists');
expect(parsed.error.suggestions).toContain('Search for the existing record');
expect(parsed.error.retryable).toBe(false);
});
it('should handle validation errors with details', () => {
const error = {
statusCode: 400,
response: {
message: 'Validation failed',
code: 'validation_error',
validation_errors: [
{
path: ['name'],
message: 'Required',
code: 'required',
},
{
path: ['email'],
message: 'Invalid email format',
expected: 'email',
received: 'invalid',
},
],
},
};
const result = handleToolError(error, 'create_person');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.VALIDATION);
expect(parsed.error.validation_errors).toHaveLength(2);
expect(parsed.error.validation_errors[0].field).toBe('name');
expect(parsed.error.validation_errors[1].field).toBe('email');
expect(parsed.error.suggestions).toContain('Provide a value for name');
expect(parsed.error.retryable).toBe(false);
});
it('should handle generic 400 Bad Request', () => {
const error = {
statusCode: 400,
message: 'Bad request',
response: {
message: 'Invalid parameters',
},
};
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.VALIDATION);
expect(parsed.error.http_status).toBe(400);
expect(parsed.error.message).toBe('Invalid parameters');
expect(parsed.error.suggestions).toContain('Check input parameters');
});
});
describe('handleToolError - Edge Cases', () => {
it('should handle standard Error objects', () => {
const error = new Error('Something went wrong');
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.UNKNOWN);
expect(parsed.error.message).toBe('Something went wrong');
expect(parsed.error.retryable).toBe(false);
});
it('should handle string errors', () => {
const error = 'Network timeout';
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.UNKNOWN);
expect(parsed.error.message).toBe('Network timeout');
});
it('should handle null/undefined errors', () => {
const error = null;
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error.type).toBe(ErrorType.UNKNOWN);
expect(parsed.error.message).toBe('null');
});
it('should include timestamp in error response', () => {
const error = new Error('test');
const result = handleToolError(error, 'test_tool');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.timestamp).toBeDefined();
expect(typeof parsed.timestamp).toBe('string');
// Should be valid ISO date
expect(new Date(parsed.timestamp).toISOString()).toBe(parsed.timestamp);
});
it('should NOT set isError flag (per ADR-003)', () => {
const error = new Error('test');
const result = handleToolError(error, 'test_tool');
// The result should NOT have isError: true
// This is intentional per our architecture decision
expect(result.isError).toBeUndefined();
});
});