/**
* Integration Tests: Error Handling
*
* Tests comprehensive error handling through MCP server:
* - Invalid API credentials
* - Network timeouts
* - Rate limit exceeded
* - Invalid tool parameters
* - Malformed responses
* - Server errors (500)
* - Resource not found (404)
* - Permission denied (403)
*
* These tests verify the server provides clear, actionable error messages
* to AI agents and handles failure scenarios gracefully.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { MCPTestClient, createMCPTestClient } from './mcp-test-client';
import { setupTestEnvironment, teardownTestEnvironment } from './setup';
import { createTask, uniqueTestName } from './utils';
describe('Error Handling Integration Tests', () => {
let client: MCPTestClient;
let testEnv: any;
let listId: string;
beforeAll(async () => {
// Setup test environment
testEnv = await setupTestEnvironment();
listId = testEnv.listId;
// Connect MCP client
client = await createMCPTestClient({
apiKey: testEnv.apiKey,
teamId: testEnv.teamId,
logRequests: false,
logResponses: false
});
});
afterAll(async () => {
// Disconnect client
await client.disconnect();
// Cleanup test environment
await teardownTestEnvironment(false);
});
describe('Invalid Parameters', () => {
it('should handle missing required parameters', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: listId
// Missing 'name' parameter
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
expect(errorText.toLowerCase()).toMatch(/error|required|name/);
});
it('should handle invalid list ID format', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: 'invalid_list',
name: 'Test Task'
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
expect(errorText.toLowerCase()).toMatch(/error|invalid|not found/);
});
it('should handle invalid task ID format', async () => {
const result = await client.callTool('clickup_task_get', {
task_id: 'invalid_task_xyz'
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
expect(errorText.toLowerCase()).toMatch(/error|invalid|not found/);
});
it('should handle invalid priority value', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Priority Test')
});
const result = await client.callTool('clickup_task_update', {
task_id: task.id,
priority: 999 // Invalid priority
});
// May error or ignore - verify response is handled
expect(result).toBeDefined();
});
it('should handle invalid status value', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Status Test')
});
const result = await client.callTool('clickup_task_update', {
task_id: task.id,
status: 'invalid_status_xyz'
});
// May error or ignore - verify response is handled
expect(result).toBeDefined();
});
it('should handle invalid date format', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: listId,
name: uniqueTestName('Date Test'),
due_date: 'not-a-valid-date'
});
// May error or parse - verify response is handled
expect(result).toBeDefined();
});
it('should handle null/undefined values', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: listId,
name: uniqueTestName('Null Test'),
description: null,
priority: undefined
});
// Should handle nulls gracefully
if (!result.isError) {
const task = client.parseJsonResult(result);
expect(task.id).toBeDefined();
}
});
it('should handle empty string parameters', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: listId,
name: '' // Empty name
});
// Should either error or create task with empty name
expect(result).toBeDefined();
});
});
describe('Resource Not Found (404)', () => {
it('should handle non-existent task ID', async () => {
const result = await client.callTool('clickup_task_get', {
task_id: 'nonexistent_task_12345'
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
expect(errorText.toLowerCase()).toMatch(/error|not found|invalid/);
});
it('should handle non-existent list ID', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: 'nonexistent_list_12345',
name: 'Test Task'
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
expect(errorText.toLowerCase()).toMatch(/error|not found|invalid/);
});
it('should handle deleted task access', async () => {
// Create and delete a task
const task = await createTask(client, listId, {
name: uniqueTestName('To Delete')
});
await client.deleteTask(task.id);
// Try to access deleted task
const result = await client.callTool('clickup_task_get', {
task_id: task.id
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
expect(errorText.toLowerCase()).toMatch(/error|not found|deleted/);
});
it('should handle non-existent comment ID', async () => {
const result = await client.callTool('clickup_task_update_comment', {
comment_id: 'nonexistent_comment_12345',
comment_text: 'Update'
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
expect(errorText.toLowerCase()).toMatch(/error|not found|invalid/);
});
it('should handle non-existent custom field ID', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Custom Field Test')
});
const result = await client.callTool('clickup_task_set_custom_field', {
task_id: task.id,
field_id: 'nonexistent_field_12345',
value: 'test',
value_type: 'text'
});
expect(result.isError).toBe(true);
});
});
describe('Invalid Operations', () => {
it('should handle circular dependency creation', async () => {
const task1 = await createTask(client, listId, {
name: uniqueTestName('Circular 1')
});
const task2 = await createTask(client, listId, {
name: uniqueTestName('Circular 2')
});
// Create dependency: task2 depends on task1
await client.addDependency(task2.id, task1.id);
// Try to create circular: task1 depends on task2
const result = await client.callTool('clickup_task_add_dependency', {
task_id: task1.id,
depends_on: task2.id
});
// ClickUp API may prevent or allow circular dependencies
// Verify response is handled appropriately
expect(result).toBeDefined();
});
it('should handle duplicate dependency', async () => {
const task1 = await createTask(client, listId, {
name: uniqueTestName('Dep 1')
});
const task2 = await createTask(client, listId, {
name: uniqueTestName('Dep 2')
});
// Add dependency
await client.addDependency(task2.id, task1.id);
// Try to add same dependency again
const result = await client.callTool('clickup_task_add_dependency', {
task_id: task2.id,
depends_on: task1.id
});
// Should handle duplicate gracefully
expect(result).toBeDefined();
});
it('should handle removing non-existent dependency', async () => {
const task1 = await createTask(client, listId, {
name: uniqueTestName('Dep Remove 1')
});
const task2 = await createTask(client, listId, {
name: uniqueTestName('Dep Remove 2')
});
// Try to remove dependency that doesn't exist
const result = await client.callTool('clickup_task_remove_dependency', {
task_id: task2.id,
depends_on: task1.id
});
// Should handle non-existent dependency gracefully
expect(result).toBeDefined();
});
it('should handle self-dependency', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Self Dep')
});
// Try to make task depend on itself
const result = await client.callTool('clickup_task_add_dependency', {
task_id: task.id,
depends_on: task.id
});
// Should error or prevent self-dependency
expect(result).toBeDefined();
});
it('should handle invalid parent task (not subtask-able)', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: listId,
name: uniqueTestName('Invalid Parent'),
parent: 'invalid_parent_12345'
});
// Should error or ignore invalid parent
expect(result).toBeDefined();
});
});
describe('Malformed Requests', () => {
it('should handle request with wrong parameter types', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: 12345, // Should be string, not number
name: 'Test'
});
// Tool should handle type coercion or error
expect(result).toBeDefined();
});
it('should handle request with extra unknown parameters', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: listId,
name: uniqueTestName('Extra Params'),
unknown_param_1: 'value',
unknown_param_2: 123,
unknown_param_3: { nested: 'object' }
});
// Should ignore unknown parameters
if (!result.isError) {
const task = client.parseJsonResult(result);
expect(task.id).toBeDefined();
}
});
it('should handle request with nested object in wrong format', async () => {
const result = await client.callTool('clickup_task_update', {
task_id: 'some_task',
assignees: 'not_an_object' // Should be object with add/rem arrays
});
// Should error or ignore malformed assignees
expect(result).toBeDefined();
});
});
describe('Permission Errors', () => {
it('should detect insufficient workspace permissions', async () => {
// This test documents expected behavior when permissions are insufficient
// Actual implementation depends on ClickUp API permissions
const result = await client.callTool('clickup_workspace_hierarchy_get', {});
// With valid API key, this should succeed
// With invalid/insufficient permissions, should error
if (result.isError) {
const errorText = client.parseTextResult(result);
expect(errorText.toLowerCase()).toMatch(/error|permission|unauthorized|forbidden/);
} else {
expect(result.isError).toBe(false);
}
});
it('should handle read-only access attempts to write', async () => {
// This test documents behavior with read-only tokens
// Can't easily test without separate read-only API key
const task = await createTask(client, listId, {
name: uniqueTestName('Permission Test')
});
// With full permissions, should succeed
const result = await client.callTool('clickup_task_delete', {
task_id: task.id
});
expect(result).toBeDefined();
});
});
describe('Error Message Quality', () => {
it('should provide clear error message for invalid task ID', async () => {
const result = await client.callTool('clickup_task_get', {
task_id: 'invalid_xyz'
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
// Error should be clear and actionable
expect(errorText.length).toBeGreaterThan(10);
expect(errorText.toLowerCase()).toMatch(/task|not found|invalid/);
});
it('should provide clear error message for missing parameters', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: listId
// Missing name
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
// Error should mention the missing parameter
expect(errorText.toLowerCase()).toMatch(/name|required/);
});
it('should provide helpful error for invalid custom field type', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Field Type Test')
});
const result = await client.callTool('clickup_task_set_custom_field', {
task_id: task.id,
field_id: 'some_field',
value: 'test',
value_type: 'invalid_type'
});
if (result.isError) {
const errorText = client.parseTextResult(result);
expect(errorText.length).toBeGreaterThan(0);
}
});
it('should include request context in error messages', async () => {
const invalidTaskId = 'context_test_12345';
const result = await client.callTool('clickup_task_get', {
task_id: invalidTaskId
});
expect(result.isError).toBe(true);
const errorText = client.parseTextResult(result);
// Error should ideally include the task ID that caused the error
// (This depends on implementation)
expect(errorText).toBeDefined();
});
});
describe('Edge Case Errors', () => {
it('should handle extremely long task names', async () => {
const longName = 'A'.repeat(10000); // 10,000 characters
const result = await client.callTool('clickup_task_create', {
list_id: listId,
name: longName
});
// May succeed (truncated) or error
expect(result).toBeDefined();
});
it('should handle special characters in task names', async () => {
const specialName = `<script>alert('xss')</script> & "quotes" 'quotes' \n\t`;
const result = await client.callTool('clickup_task_create', {
list_id: listId,
name: specialName
});
// Should handle special characters safely
if (!result.isError) {
const task = client.parseJsonResult(result);
expect(task.id).toBeDefined();
}
});
it('should handle unicode and emoji in task names', async () => {
const unicodeName = '🚀 Task with émojis and ünicode 你好 مرحبا';
const task = await createTask(client, listId, {
name: unicodeName
});
expect(task.id).toBeDefined();
expect(task.name).toBe(unicodeName);
});
it('should handle very large numbers', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: listId,
name: uniqueTestName('Large Number'),
due_date: 9999999999999 // Far future timestamp
});
// Should handle or error gracefully
expect(result).toBeDefined();
});
it('should handle negative numbers where invalid', async () => {
const result = await client.callTool('clickup_task_create', {
list_id: listId,
name: uniqueTestName('Negative Number'),
priority: -1 // Invalid priority
});
// Should handle invalid priority
expect(result).toBeDefined();
});
it('should handle concurrent modifications', async () => {
const task = await createTask(client, listId, {
name: uniqueTestName('Concurrent Mod')
});
// Attempt concurrent updates
const update1Promise = client.updateTask(task.id, {
description: 'Update 1'
});
const update2Promise = client.updateTask(task.id, {
description: 'Update 2'
});
const [result1, result2] = await Promise.all([update1Promise, update2Promise]);
// Both should succeed (last write wins)
expect(result1.id).toBeDefined();
expect(result2.id).toBeDefined();
});
});
describe('Recovery from Errors', () => {
it('should allow retry after failed operation', async () => {
// Attempt operation with invalid ID
const failResult = await client.callTool('clickup_task_get', {
task_id: 'invalid_id'
});
expect(failResult.isError).toBe(true);
// Create valid task and retry
const task = await createTask(client, listId, {
name: uniqueTestName('Retry Test')
});
const successResult = await client.callTool('clickup_task_get', {
task_id: task.id
});
expect(successResult.isError).toBe(false);
const retrieved = client.parseJsonResult(successResult);
expect(retrieved.id).toBe(task.id);
});
it('should maintain connection after error', async () => {
// Generate error
const errorResult = await client.callTool('clickup_task_get', {
task_id: 'invalid'
});
expect(errorResult.isError).toBe(true);
// Verify connection still works
const listToolsResult = await client.listTools();
expect(listToolsResult).toBeDefined();
expect(listToolsResult.tools).toBeDefined();
expect(listToolsResult.tools.length).toBeGreaterThan(0);
});
it('should handle multiple consecutive errors', async () => {
const invalidIds = ['invalid1', 'invalid2', 'invalid3', 'invalid4', 'invalid5'];
for (const id of invalidIds) {
const result = await client.callTool('clickup_task_get', {
task_id: id
});
expect(result.isError).toBe(true);
}
// Verify client still functional
const task = await createTask(client, listId, {
name: uniqueTestName('After Errors')
});
expect(task.id).toBeDefined();
});
});
describe('Error Logging and Debugging', () => {
it('should log errors for debugging', async () => {
// This test verifies error logging behavior
const result = await client.callTool('clickup_task_get', {
task_id: 'log_test_invalid'
});
expect(result.isError).toBe(true);
// Error details should be available
const errorText = client.parseTextResult(result);
expect(errorText.length).toBeGreaterThan(0);
});
it('should preserve error details across tool calls', async () => {
// Generate error
const errorResult = await client.callTool('clickup_task_get', {
task_id: 'error_details_test'
});
expect(errorResult.isError).toBe(true);
const errorText1 = client.parseTextResult(errorResult);
// Make successful call
const task = await createTask(client, listId, {
name: uniqueTestName('Success After Error')
});
expect(task.id).toBeDefined();
// Error from first call should still be accessible if logged
expect(errorText1.length).toBeGreaterThan(0);
});
});
});