Skip to main content
Glama
todoist-tasks-completed.test.ts14.9 kB
import { describe, test, expect, beforeEach, jest } from '@jest/globals'; import { TodoistTasksTool } from '../../src/tools/todoist-tasks.js'; import { CacheService } from '../../src/services/cache.js'; import { TodoistApiService } from '../../src/services/todoist-api.js'; import { BatchOperationsService, BatchOperationRequest, } from '../../src/services/batch.js'; import { BatchOperationResult } from '../../src/types/errors.js'; import { TodoistTask } from '../../src/types/todoist.js'; const mockApiConfig = { token: 'test_token_123456', base_url: 'https://api.todoist.com/rest/v1', timeout: 10000, retry_attempts: 3, }; // Mock completed tasks data const mockCompletedTask1: TodoistTask = { id: '8765432109', content: 'Complete project proposal', description: 'Draft and submit Q4 proposal', project_id: '2345678901', section_id: null as any, parent_id: null as any, order: 1, priority: 4, labels: ['Work', 'Urgent'], assignee_id: undefined, assigner_id: undefined, comment_count: 0, completed: true, completed_at: '2025-09-15T14:30:00Z', due: { date: '2025-09-15', string: '2025-09-15', is_recurring: false, }, url: 'https://todoist.com/showTask?id=8765432109', created_at: '2025-09-01T09:00:00Z', creator_id: '123456', }; const mockCompletedTask2: TodoistTask = { id: '8765432110', content: 'Team meeting notes', description: '', project_id: '2345678901', section_id: null as any, parent_id: null as any, order: 2, priority: 2, labels: ['Work'], assignee_id: undefined, assigner_id: undefined, comment_count: 0, completed: true, completed_at: '2025-09-16T10:00:00Z', due: { date: '2025-09-16', string: '2025-09-16', is_recurring: false, }, url: 'https://todoist.com/showTask?id=8765432110', created_at: '2025-09-05T12:00:00Z', creator_id: '123456', }; type CompletedTasksApiMock = jest.Mocked< Pick< TodoistApiService, | 'getCompletedTasksByCompletionDate' | 'getCompletedTasksByDueDate' | 'getRateLimitStatus' > >; // Type guard for completed tasks response function isCompletedTasksResponse( data: unknown ): data is { items: TodoistTask[]; next_cursor: string | null } { return ( typeof data === 'object' && data !== null && 'items' in data && Array.isArray((data as { items: unknown }).items) ); } function createCompletedTasksApiMock(): CompletedTasksApiMock { return { getCompletedTasksByCompletionDate: jest.fn(async () => ({ items: [mockCompletedTask1, mockCompletedTask2], next_cursor: null, })), getCompletedTasksByDueDate: jest.fn(async () => ({ items: [mockCompletedTask1], next_cursor: null, })), getRateLimitStatus: jest.fn(() => ({ sync: { remaining: 50, resetTime: new Date(Date.now() + 60000), isLimited: false, }, rest: { remaining: 300, resetTime: new Date(Date.now() + 60000), isLimited: false, }, })), } as unknown as CompletedTasksApiMock; } type BatchServiceMock = jest.Mocked< Pick<BatchOperationsService, 'executeBatch'> >; function createMockBatchService(): BatchServiceMock { const result: BatchOperationResult = { success: true, completed_commands: 0, failed_commands: 0, errors: [], temp_id_mapping: {}, }; const executeBatch = jest.fn( async (_request: BatchOperationRequest): Promise<BatchOperationResult> => result ); return { executeBatch, } as unknown as BatchServiceMock; } describe('todoist_tasks MCP Tool - list_completed action (by_completion_date)', () => { let apiService: CompletedTasksApiMock; let batchService: BatchServiceMock; let todoistTasksTool: TodoistTasksTool; beforeEach(() => { apiService = createCompletedTasksApiMock(); batchService = createMockBatchService(); todoistTasksTool = new TodoistTasksTool(mockApiConfig, { apiService: apiService as unknown as TodoistApiService, batchService: batchService as unknown as BatchOperationsService, cacheService: new CacheService(), }); }); // T002: Contract tests for list_completed with by_completion_date describe('by_completion_date query type', () => { test('1. Valid request with required params only', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, since: '2025-09-01T00:00:00Z', until: '2025-10-01T23:59:59Z', }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(isCompletedTasksResponse(result.data)).toBe(true); if (isCompletedTasksResponse(result.data)) { expect(result.data.items).toBeInstanceOf(Array); if (isCompletedTasksResponse(result.data)) { expect(result.data.next_cursor).toBeDefined(); } } expect(apiService.getCompletedTasksByCompletionDate).toHaveBeenCalledWith( expect.objectContaining({ since: '2025-09-01T00:00:00Z', until: '2025-10-01T23:59:59Z', }) ); }); test('2. Valid request with project filter', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, since: '2025-09-01T00:00:00Z', until: '2025-10-01T23:59:59Z', project_id: '2345678901', }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(true); if (isCompletedTasksResponse(result.data)) { expect(result.data.items).toBeInstanceOf(Array); // All items should have matching project_id result.data.items.forEach((task: TodoistTask) => { expect(task.project_id).toBe('2345678901'); }); } expect(apiService.getCompletedTasksByCompletionDate).toHaveBeenCalledWith( expect.objectContaining({ project_id: '2345678901', }) ); }); test('3. Valid request with filter query', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, since: '2025-09-01T00:00:00Z', until: '2025-10-01T23:59:59Z', filter_query: '@Work', }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(true); if (isCompletedTasksResponse(result.data)) { expect(result.data.items).toBeInstanceOf(Array); // All items should have "Work" label result.data.items.forEach((task: TodoistTask) => { expect(task.labels).toContain('Work'); }); } expect(apiService.getCompletedTasksByCompletionDate).toHaveBeenCalledWith( expect.objectContaining({ filter_query: '@Work', }) ); }); test('4. Time window exactly 3 months (boundary test)', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, since: '2025-07-01T00:00:00Z', until: '2025-10-01T00:00:00Z', // Exactly 92 days }; const result = await todoistTasksTool.execute(params); // Should succeed at boundary expect(result.success).toBe(true); expect(apiService.getCompletedTasksByCompletionDate).toHaveBeenCalled(); }); test('5. Time window exceeds 3 months (should error)', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, since: '2025-06-01T00:00:00Z', until: '2025-10-01T00:00:00Z', // More than 92 days }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(false); expect(result.error?.code).toBe('VALIDATION_ERROR'); expect(result.error?.message).toContain('92 days'); expect(result.error?.message).toContain('completion date'); }); test('6. Invalid datetime format (should error)', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, since: '2025-09-01', // Missing time component until: '2025-10-01', }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(false); expect(result.error?.code).toBe('VALIDATION_ERROR'); expect(result.error?.message).toContain('ISO 8601'); }); test('7. Missing required parameter since (should error)', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, until: '2025-10-01T00:00:00Z', // Missing 'since' }; const result = await todoistTasksTool.execute(params as any); expect(result.success).toBe(false); expect(result.error?.code).toBe('VALIDATION_ERROR'); expect(result.error?.message).toContain('since'); }); test('8. Until before since (should error)', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, since: '2025-10-01T00:00:00Z', until: '2025-09-01T00:00:00Z', // Before since }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(false); expect(result.error?.code).toBe('VALIDATION_ERROR'); expect(result.error?.message).toContain('after since'); }); test('9. Pagination with cursor', async () => { // Mock returns cursor for next page apiService.getCompletedTasksByCompletionDate = jest.fn(async () => ({ items: [mockCompletedTask1], next_cursor: 'eyJwYWdlIjoyLCJsYXN0X2lkIjoiODc2NTQzMjEwOSJ9', })); const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, since: '2025-09-01T00:00:00Z', until: '2025-10-01T23:59:59Z', cursor: 'eyJwYWdlIjoxfQ==', }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(true); if (isCompletedTasksResponse(result.data)) { expect(result.data.next_cursor).toBeDefined(); } expect(apiService.getCompletedTasksByCompletionDate).toHaveBeenCalledWith( expect.objectContaining({ cursor: 'eyJwYWdlIjoxfQ==', }) ); }); test('10. Custom limit parameter', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_completion_date' as const, since: '2025-09-01T00:00:00Z', until: '2025-10-01T23:59:59Z', limit: 10, }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(true); if (isCompletedTasksResponse(result.data)) { expect(result.data.items.length).toBeLessThanOrEqual(10); } expect(apiService.getCompletedTasksByCompletionDate).toHaveBeenCalledWith( expect.objectContaining({ limit: 10, }) ); }); }); // T003: Contract tests for list_completed with by_due_date describe('by_due_date query type', () => { test('1. Valid request with required params only', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_due_date' as const, since: '2025-09-15T00:00:00Z', until: '2025-10-01T23:59:59Z', }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(true); expect(result.data).toBeDefined(); if (isCompletedTasksResponse(result.data)) { expect(result.data.items).toBeInstanceOf(Array); } expect(apiService.getCompletedTasksByDueDate).toHaveBeenCalledWith( expect.objectContaining({ since: '2025-09-15T00:00:00Z', until: '2025-10-01T23:59:59Z', }) ); }); test('2. Time window exactly 6 weeks (boundary test)', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_due_date' as const, since: '2025-08-20T00:00:00Z', until: '2025-10-01T00:00:00Z', // Exactly 42 days }; const result = await todoistTasksTool.execute(params); // Should succeed at boundary expect(result.success).toBe(true); expect(apiService.getCompletedTasksByDueDate).toHaveBeenCalled(); }); test('3. Time window exceeds 6 weeks (should error)', async () => { const params = { action: 'list_completed' as const, completed_query_type: 'by_due_date' as const, since: '2025-08-01T00:00:00Z', until: '2025-10-01T00:00:00Z', // More than 42 days }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(false); expect(result.error?.code).toBe('VALIDATION_ERROR'); expect(result.error?.message).toContain('42 days'); expect(result.error?.message).toContain('due date'); }); test('4. Tasks with no due date excluded', async () => { // Mock returns tasks, all should have due dates const params = { action: 'list_completed' as const, completed_query_type: 'by_due_date' as const, since: '2025-09-15T00:00:00Z', until: '2025-10-01T23:59:59Z', }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(true); if (isCompletedTasksResponse(result.data)) { // Verify no tasks with null due date result.data.items.forEach((task: TodoistTask) => { expect(task.due).not.toBeNull(); expect(task.due).toBeDefined(); }); } }); test('5. Recurring task due dates handled', async () => { const recurringTask: TodoistTask = { ...mockCompletedTask1, id: '8765432111', content: 'Weekly status report', due: { date: '2025-09-20', string: 'every Friday', is_recurring: true, }, }; apiService.getCompletedTasksByDueDate = jest.fn(async () => ({ items: [recurringTask], next_cursor: null, })); const params = { action: 'list_completed' as const, completed_query_type: 'by_due_date' as const, since: '2025-09-15T00:00:00Z', until: '2025-10-01T23:59:59Z', }; const result = await todoistTasksTool.execute(params); expect(result.success).toBe(true); if (isCompletedTasksResponse(result.data)) { expect(result.data.items[0].due?.is_recurring).toBe(true); expect(result.data.items[0].due?.date).toBeDefined(); } }); }); });

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/shayonpal/mcp-todoist'

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