@doist/todoist-ai 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/index.d.ts +29 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -48
- package/dist/main.js +6 -11
- package/dist/mcp-helpers.d.ts +2 -2
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-helpers.js +1 -4
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +34 -36
- package/dist/todoist-tool.js +1 -2
- package/dist/tool-helpers.d.ts +13 -1
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +43 -22
- package/dist/tool-helpers.test.js +55 -14
- package/dist/tools/__tests__/delete-one.test.d.ts +2 -0
- package/dist/tools/__tests__/delete-one.test.d.ts.map +1 -0
- package/dist/tools/__tests__/delete-one.test.js +90 -0
- package/dist/tools/__tests__/overview.test.d.ts +2 -0
- package/dist/tools/__tests__/overview.test.d.ts.map +1 -0
- package/dist/tools/__tests__/overview.test.js +163 -0
- package/dist/tools/__tests__/projects-list.test.d.ts +2 -0
- package/dist/tools/__tests__/projects-list.test.d.ts.map +1 -0
- package/dist/tools/__tests__/projects-list.test.js +140 -0
- package/dist/tools/__tests__/projects-manage.test.d.ts +2 -0
- package/dist/tools/__tests__/projects-manage.test.d.ts.map +1 -0
- package/dist/tools/__tests__/projects-manage.test.js +106 -0
- package/dist/tools/__tests__/sections-manage.test.d.ts +2 -0
- package/dist/tools/__tests__/sections-manage.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sections-manage.test.js +138 -0
- package/dist/tools/__tests__/sections-search.test.d.ts +2 -0
- package/dist/tools/__tests__/sections-search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sections-search.test.js +235 -0
- package/dist/tools/__tests__/tasks-add-multiple.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-add-multiple.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-add-multiple.test.js +274 -0
- package/dist/tools/__tests__/tasks-complete-multiple.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-complete-multiple.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-complete-multiple.test.js +146 -0
- package/dist/tools/__tests__/tasks-list-by-date.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-list-by-date.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-list-by-date.test.js +192 -0
- package/dist/tools/__tests__/tasks-list-completed.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-list-completed.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-list-completed.test.js +154 -0
- package/dist/tools/__tests__/tasks-list-for-container.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-list-for-container.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-list-for-container.test.js +232 -0
- package/dist/tools/__tests__/tasks-organize-multiple.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-organize-multiple.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-organize-multiple.test.js +245 -0
- package/dist/tools/__tests__/tasks-search.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-search.test.js +106 -0
- package/dist/tools/__tests__/tasks-update-one.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-update-one.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-update-one.test.js +251 -0
- package/dist/tools/delete-one.js +4 -7
- package/dist/tools/overview.js +8 -11
- package/dist/tools/projects-list.js +7 -10
- package/dist/tools/projects-manage.js +6 -9
- package/dist/tools/sections-manage.js +7 -10
- package/dist/tools/sections-search.js +4 -7
- package/dist/tools/tasks-add-multiple.d.ts +5 -0
- package/dist/tools/tasks-add-multiple.d.ts.map +1 -1
- package/dist/tools/tasks-add-multiple.js +37 -17
- package/dist/tools/tasks-complete-multiple.js +3 -6
- package/dist/tools/tasks-list-by-date.d.ts +1 -0
- package/dist/tools/tasks-list-by-date.d.ts.map +1 -1
- package/dist/tools/tasks-list-by-date.js +12 -15
- package/dist/tools/tasks-list-completed.d.ts +2 -1
- package/dist/tools/tasks-list-completed.d.ts.map +1 -1
- package/dist/tools/tasks-list-completed.js +13 -16
- package/dist/tools/tasks-list-for-container.d.ts +1 -0
- package/dist/tools/tasks-list-for-container.d.ts.map +1 -1
- package/dist/tools/tasks-list-for-container.js +8 -11
- package/dist/tools/tasks-organize-multiple.d.ts.map +1 -1
- package/dist/tools/tasks-organize-multiple.js +20 -14
- package/dist/tools/tasks-search.d.ts +1 -0
- package/dist/tools/tasks-search.d.ts.map +1 -1
- package/dist/tools/tasks-search.js +7 -10
- package/dist/tools/tasks-update-one.d.ts +4 -2
- package/dist/tools/tasks-update-one.d.ts.map +1 -1
- package/dist/tools/tasks-update-one.js +45 -15
- package/dist/tools/test-helpers.d.ts +80 -0
- package/dist/tools/test-helpers.d.ts.map +1 -0
- package/dist/tools/test-helpers.js +140 -0
- package/dist/utils/duration-parser.d.ts +36 -0
- package/dist/utils/duration-parser.d.ts.map +1 -0
- package/dist/utils/duration-parser.js +96 -0
- package/dist/utils/duration-parser.test.d.ts +2 -0
- package/dist/utils/duration-parser.test.d.ts.map +1 -0
- package/dist/utils/duration-parser.test.js +147 -0
- package/package.json +6 -2
- package/scripts/test-executable.cjs +69 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { tasksCompleteMultiple } from '../tasks-complete-multiple.js';
|
|
3
|
+
// Mock the Todoist API
|
|
4
|
+
const mockTodoistApi = {
|
|
5
|
+
closeTask: jest.fn(),
|
|
6
|
+
};
|
|
7
|
+
describe('tasks-complete-multiple tool', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.clearAllMocks();
|
|
10
|
+
});
|
|
11
|
+
describe('completing multiple tasks', () => {
|
|
12
|
+
it('should complete all tasks successfully', async () => {
|
|
13
|
+
mockTodoistApi.closeTask.mockResolvedValue(true);
|
|
14
|
+
const result = await tasksCompleteMultiple.execute({ ids: ['task-1', 'task-2', 'task-3'] }, mockTodoistApi);
|
|
15
|
+
// Verify API was called for each task
|
|
16
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(3);
|
|
17
|
+
expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(1, 'task-1');
|
|
18
|
+
expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(2, 'task-2');
|
|
19
|
+
expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(3, 'task-3');
|
|
20
|
+
// Verify all tasks were completed successfully
|
|
21
|
+
expect(result).toEqual({
|
|
22
|
+
success: true,
|
|
23
|
+
completed: ['task-1', 'task-2', 'task-3'],
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
it('should complete single task', async () => {
|
|
27
|
+
mockTodoistApi.closeTask.mockResolvedValue(true);
|
|
28
|
+
const result = await tasksCompleteMultiple.execute({ ids: ['8485093748'] }, mockTodoistApi);
|
|
29
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(1);
|
|
30
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledWith('8485093748');
|
|
31
|
+
expect(result).toEqual({
|
|
32
|
+
success: true,
|
|
33
|
+
completed: ['8485093748'],
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it('should handle partial failures gracefully', async () => {
|
|
37
|
+
// Mock first and third tasks to succeed, second to fail
|
|
38
|
+
mockTodoistApi.closeTask
|
|
39
|
+
.mockResolvedValueOnce(true) // task-1 succeeds
|
|
40
|
+
.mockRejectedValueOnce(new Error('Task not found')) // task-2 fails
|
|
41
|
+
.mockResolvedValueOnce(true); // task-3 succeeds
|
|
42
|
+
const result = await tasksCompleteMultiple.execute({ ids: ['task-1', 'task-2', 'task-3'] }, mockTodoistApi);
|
|
43
|
+
// Verify API was called for all tasks despite failure
|
|
44
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(3);
|
|
45
|
+
expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(1, 'task-1');
|
|
46
|
+
expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(2, 'task-2');
|
|
47
|
+
expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(3, 'task-3');
|
|
48
|
+
// Verify only successful completions are reported
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
success: true,
|
|
51
|
+
completed: ['task-1', 'task-3'], // task-2 excluded due to failure
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it('should handle all tasks failing', async () => {
|
|
55
|
+
const apiError = new Error('API Error: Network timeout');
|
|
56
|
+
mockTodoistApi.closeTask.mockRejectedValue(apiError);
|
|
57
|
+
const result = await tasksCompleteMultiple.execute({ ids: ['task-1', 'task-2'] }, mockTodoistApi);
|
|
58
|
+
// Verify API was attempted for all tasks
|
|
59
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(2);
|
|
60
|
+
// Verify no tasks were completed but still returns success
|
|
61
|
+
expect(result).toEqual({
|
|
62
|
+
success: true,
|
|
63
|
+
completed: [], // no tasks completed
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it('should continue processing remaining tasks after failures', async () => {
|
|
67
|
+
// Mock various failure scenarios
|
|
68
|
+
mockTodoistApi.closeTask
|
|
69
|
+
.mockRejectedValueOnce(new Error('Task already completed'))
|
|
70
|
+
.mockRejectedValueOnce(new Error('Task not found'))
|
|
71
|
+
.mockResolvedValueOnce(true) // task-3 succeeds
|
|
72
|
+
.mockRejectedValueOnce(new Error('Permission denied'))
|
|
73
|
+
.mockResolvedValueOnce(true); // task-5 succeeds
|
|
74
|
+
const result = await tasksCompleteMultiple.execute({ ids: ['task-1', 'task-2', 'task-3', 'task-4', 'task-5'] }, mockTodoistApi);
|
|
75
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(5);
|
|
76
|
+
// Only tasks 3 and 5 should be in completed list
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
success: true,
|
|
79
|
+
completed: ['task-3', 'task-5'],
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it('should handle different types of API errors', async () => {
|
|
83
|
+
mockTodoistApi.closeTask
|
|
84
|
+
.mockRejectedValueOnce(new Error('Task not found'))
|
|
85
|
+
.mockRejectedValueOnce(new Error('Task already completed'))
|
|
86
|
+
.mockRejectedValueOnce(new Error('Permission denied'))
|
|
87
|
+
.mockRejectedValueOnce(new Error('Rate limit exceeded'));
|
|
88
|
+
const result = await tasksCompleteMultiple.execute({ ids: ['not-found', 'already-done', 'no-permission', 'rate-limited'] }, mockTodoistApi);
|
|
89
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(4);
|
|
90
|
+
// All should fail, but the tool should handle it gracefully
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
success: true,
|
|
93
|
+
completed: [],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('mixed success and failure scenarios', () => {
|
|
98
|
+
it('should handle realistic mixed scenario', async () => {
|
|
99
|
+
// Simulate a realistic scenario with some tasks completing and others failing
|
|
100
|
+
mockTodoistApi.closeTask
|
|
101
|
+
.mockResolvedValueOnce(true) // regular task completion
|
|
102
|
+
.mockResolvedValueOnce(true) // another successful completion
|
|
103
|
+
.mockRejectedValueOnce(new Error('Task already completed')) // duplicate completion
|
|
104
|
+
.mockResolvedValueOnce(true) // successful completion
|
|
105
|
+
.mockRejectedValueOnce(new Error('Task not found')); // deleted task
|
|
106
|
+
const result = await tasksCompleteMultiple.execute({
|
|
107
|
+
ids: [
|
|
108
|
+
'8485093748', // regular task
|
|
109
|
+
'8485093749', // regular task
|
|
110
|
+
'8485093750', // already completed
|
|
111
|
+
'8485093751', // regular task
|
|
112
|
+
'8485093752', // deleted task
|
|
113
|
+
],
|
|
114
|
+
}, mockTodoistApi);
|
|
115
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(5);
|
|
116
|
+
expect(result).toEqual({
|
|
117
|
+
success: true,
|
|
118
|
+
completed: ['8485093748', '8485093749', '8485093751'],
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('edge cases', () => {
|
|
123
|
+
it('should handle empty task completion (minimum one task required by schema)', async () => {
|
|
124
|
+
// Note: This test documents that the schema requires at least one task,
|
|
125
|
+
// so this scenario shouldn't occur in practice due to validation
|
|
126
|
+
mockTodoistApi.closeTask.mockResolvedValue(true);
|
|
127
|
+
const result = await tasksCompleteMultiple.execute({ ids: ['single-task'] }, mockTodoistApi);
|
|
128
|
+
expect(result).toEqual({
|
|
129
|
+
success: true,
|
|
130
|
+
completed: ['single-task'],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
it('should handle tasks with special ID formats', async () => {
|
|
134
|
+
mockTodoistApi.closeTask.mockResolvedValue(true);
|
|
135
|
+
const result = await tasksCompleteMultiple.execute({ ids: ['proj_123_task_456', 'task-with-dashes', '1234567890'] }, mockTodoistApi);
|
|
136
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(3);
|
|
137
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledWith('proj_123_task_456');
|
|
138
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledWith('task-with-dashes');
|
|
139
|
+
expect(mockTodoistApi.closeTask).toHaveBeenCalledWith('1234567890');
|
|
140
|
+
expect(result).toEqual({
|
|
141
|
+
success: true,
|
|
142
|
+
completed: ['proj_123_task_456', 'task-with-dashes', '1234567890'],
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-list-by-date.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-list-by-date.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
+
import { tasksListByDate } from '../tasks-list-by-date.js';
|
|
4
|
+
import { TEST_ERRORS, TEST_IDS, createMappedTask } from '../test-helpers.js';
|
|
5
|
+
// Mock the tool helpers
|
|
6
|
+
jest.mock('../../tool-helpers', () => ({
|
|
7
|
+
getTasksByFilter: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
const mockGetTasksByFilter = getTasksByFilter;
|
|
10
|
+
// Mock the Todoist API (not directly used by tasks-list-by-date, but needed for type)
|
|
11
|
+
const mockTodoistApi = {};
|
|
12
|
+
// Mock date-fns functions to make tests deterministic
|
|
13
|
+
jest.mock('date-fns', () => ({
|
|
14
|
+
addDays: jest.fn(() => new Date('2025-08-16')), // Return predictable end date
|
|
15
|
+
formatISO: jest.fn((date, options) => {
|
|
16
|
+
if (typeof date === 'string') {
|
|
17
|
+
return date; // Return string dates as-is
|
|
18
|
+
}
|
|
19
|
+
if (options &&
|
|
20
|
+
typeof options === 'object' &&
|
|
21
|
+
'representation' in options &&
|
|
22
|
+
options.representation === 'date') {
|
|
23
|
+
return '2025-08-15'; // Return predictable date for 'today'
|
|
24
|
+
}
|
|
25
|
+
return '2025-08-16'; // Return predictable end date
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
describe('tasks-list-by-date tool', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
// Mock current date to make tests deterministic
|
|
32
|
+
jest.spyOn(Date, 'now').mockReturnValue(new Date('2025-08-15T10:00:00Z').getTime());
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
jest.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
describe('listing overdue tasks', () => {
|
|
38
|
+
it.each([
|
|
39
|
+
{ daysCount: 7, hasTasks: true, description: 'with tasks' },
|
|
40
|
+
{ daysCount: 5, hasTasks: false, description: 'ignoring daysCount' },
|
|
41
|
+
])('should handle overdue tasks $description', async ({ daysCount, hasTasks }) => {
|
|
42
|
+
const mockTasks = hasTasks
|
|
43
|
+
? [
|
|
44
|
+
createMappedTask({
|
|
45
|
+
id: TEST_IDS.TASK_1,
|
|
46
|
+
content: 'Overdue task',
|
|
47
|
+
dueDate: '2025-08-10',
|
|
48
|
+
priority: 2,
|
|
49
|
+
labels: ['urgent'],
|
|
50
|
+
}),
|
|
51
|
+
]
|
|
52
|
+
: [];
|
|
53
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
54
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
55
|
+
const result = await tasksListByDate.execute({ startDate: 'overdue', limit: 50, daysCount }, mockTodoistApi);
|
|
56
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
57
|
+
client: mockTodoistApi,
|
|
58
|
+
query: 'overdue',
|
|
59
|
+
cursor: undefined,
|
|
60
|
+
limit: 50,
|
|
61
|
+
});
|
|
62
|
+
expect(result).toEqual(mockResponse);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('listing tasks by date range', () => {
|
|
66
|
+
it('should get tasks for today when startDate is "today"', async () => {
|
|
67
|
+
const mockTasks = [createMappedTask({ content: 'Today task', dueDate: '2025-08-15' })];
|
|
68
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
69
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
70
|
+
const result = await tasksListByDate.execute({ startDate: 'today', limit: 50, daysCount: 7 }, mockTodoistApi);
|
|
71
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
72
|
+
client: mockTodoistApi,
|
|
73
|
+
query: expect.stringContaining('due after:') && expect.stringContaining('due before:'),
|
|
74
|
+
cursor: undefined,
|
|
75
|
+
limit: 50,
|
|
76
|
+
});
|
|
77
|
+
expect(result).toEqual(mockResponse);
|
|
78
|
+
});
|
|
79
|
+
it.each([
|
|
80
|
+
{
|
|
81
|
+
name: 'specific date',
|
|
82
|
+
params: { startDate: '2025-08-20', limit: 50, daysCount: 7 },
|
|
83
|
+
tasks: [createMappedTask({ content: 'Specific date task', dueDate: '2025-08-20' })],
|
|
84
|
+
cursor: null,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'multiple days with pagination',
|
|
88
|
+
params: {
|
|
89
|
+
startDate: '2025-08-20',
|
|
90
|
+
daysCount: 3,
|
|
91
|
+
limit: 20,
|
|
92
|
+
cursor: 'current-cursor',
|
|
93
|
+
},
|
|
94
|
+
tasks: [
|
|
95
|
+
createMappedTask({
|
|
96
|
+
id: TEST_IDS.TASK_2,
|
|
97
|
+
content: 'Multi-day task 1',
|
|
98
|
+
dueDate: '2025-08-20',
|
|
99
|
+
}),
|
|
100
|
+
createMappedTask({
|
|
101
|
+
id: TEST_IDS.TASK_3,
|
|
102
|
+
content: 'Multi-day task 2',
|
|
103
|
+
dueDate: '2025-08-21',
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
cursor: 'next-page-cursor',
|
|
107
|
+
},
|
|
108
|
+
])('should handle $name', async ({ params, tasks, cursor }) => {
|
|
109
|
+
const mockResponse = { tasks, nextCursor: cursor };
|
|
110
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
111
|
+
const result = await tasksListByDate.execute(params, mockTodoistApi);
|
|
112
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
113
|
+
client: mockTodoistApi,
|
|
114
|
+
query: expect.stringContaining('2025-08-20'),
|
|
115
|
+
cursor: params.cursor || undefined,
|
|
116
|
+
limit: params.limit,
|
|
117
|
+
});
|
|
118
|
+
expect(result).toEqual(mockResponse);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('pagination and limits', () => {
|
|
122
|
+
it.each([
|
|
123
|
+
{
|
|
124
|
+
name: 'pagination parameters',
|
|
125
|
+
params: {
|
|
126
|
+
startDate: 'today',
|
|
127
|
+
limit: 25,
|
|
128
|
+
daysCount: 7,
|
|
129
|
+
cursor: 'pagination-cursor',
|
|
130
|
+
},
|
|
131
|
+
expectedCursor: 'pagination-cursor',
|
|
132
|
+
expectedLimit: 25,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'default values',
|
|
136
|
+
params: { startDate: '2025-08-15', limit: 50, daysCount: 7 },
|
|
137
|
+
expectedCursor: undefined,
|
|
138
|
+
expectedLimit: 50,
|
|
139
|
+
},
|
|
140
|
+
])('should handle $name', async ({ params, expectedCursor, expectedLimit }) => {
|
|
141
|
+
const mockResponse = { tasks: [], nextCursor: null };
|
|
142
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
143
|
+
await tasksListByDate.execute(params, mockTodoistApi);
|
|
144
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
145
|
+
client: mockTodoistApi,
|
|
146
|
+
query: expect.any(String),
|
|
147
|
+
cursor: expectedCursor,
|
|
148
|
+
limit: expectedLimit,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('edge cases', () => {
|
|
153
|
+
it.each([
|
|
154
|
+
{ name: 'empty results', daysCount: 7, shouldReturnResult: true },
|
|
155
|
+
{ name: 'maximum daysCount', daysCount: 30, shouldReturnResult: false },
|
|
156
|
+
{ name: 'minimum daysCount', daysCount: 1, shouldReturnResult: false },
|
|
157
|
+
])('should handle $name', async ({ daysCount, shouldReturnResult }) => {
|
|
158
|
+
const mockResponse = { tasks: [], nextCursor: null };
|
|
159
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
160
|
+
const startDate = daysCount === 7 ? 'today' : '2025-08-15';
|
|
161
|
+
const result = await tasksListByDate.execute({ startDate, limit: 50, daysCount }, mockTodoistApi);
|
|
162
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledTimes(1);
|
|
163
|
+
if (shouldReturnResult) {
|
|
164
|
+
expect(result).toEqual(mockResponse);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('error handling', () => {
|
|
169
|
+
it.each([
|
|
170
|
+
{
|
|
171
|
+
error: TEST_ERRORS.INVALID_FILTER,
|
|
172
|
+
params: { startDate: 'today', limit: 50, daysCount: 7 },
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
error: TEST_ERRORS.API_RATE_LIMIT,
|
|
176
|
+
params: { startDate: 'overdue', limit: 50, daysCount: 7 },
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
error: TEST_ERRORS.INVALID_CURSOR,
|
|
180
|
+
params: {
|
|
181
|
+
startDate: '2025-08-15',
|
|
182
|
+
limit: 50,
|
|
183
|
+
daysCount: 7,
|
|
184
|
+
cursor: 'invalid-cursor',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
])('should propagate $error', async ({ error, params }) => {
|
|
188
|
+
mockGetTasksByFilter.mockRejectedValue(new Error(error));
|
|
189
|
+
await expect(tasksListByDate.execute(params, mockTodoistApi)).rejects.toThrow(error);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-list-completed.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-list-completed.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { tasksListCompleted } from '../tasks-list-completed.js';
|
|
3
|
+
import { createMockTask } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
getCompletedTasksByCompletionDate: jest.fn(),
|
|
7
|
+
getCompletedTasksByDueDate: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
describe('tasks-list-completed tool', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
describe('getting completed tasks by completion date (default)', () => {
|
|
14
|
+
it('should get completed tasks by completion date', async () => {
|
|
15
|
+
const mockCompletedTasks = [
|
|
16
|
+
createMockTask({
|
|
17
|
+
id: '8485093748',
|
|
18
|
+
content: 'Completed task 1',
|
|
19
|
+
description: 'Task completed yesterday',
|
|
20
|
+
completedAt: '2024-01-01T00:00:00Z',
|
|
21
|
+
labels: ['work'],
|
|
22
|
+
priority: 2,
|
|
23
|
+
url: 'https://todoist.com/showTask?id=8485093748',
|
|
24
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
25
|
+
due: {
|
|
26
|
+
date: '2025-08-14',
|
|
27
|
+
isRecurring: false,
|
|
28
|
+
lang: 'en',
|
|
29
|
+
string: 'Aug 14',
|
|
30
|
+
timezone: null,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
];
|
|
34
|
+
mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue({
|
|
35
|
+
items: mockCompletedTasks,
|
|
36
|
+
nextCursor: null,
|
|
37
|
+
});
|
|
38
|
+
const result = await tasksListCompleted.execute({ getBy: 'completion', limit: 50, since: '2025-08-10', until: '2025-08-15' }, mockTodoistApi);
|
|
39
|
+
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
40
|
+
since: '2025-08-10',
|
|
41
|
+
until: '2025-08-15',
|
|
42
|
+
limit: 50,
|
|
43
|
+
});
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
tasks: [
|
|
46
|
+
expect.objectContaining({
|
|
47
|
+
id: '8485093748',
|
|
48
|
+
content: 'Completed task 1',
|
|
49
|
+
description: 'Task completed yesterday',
|
|
50
|
+
dueDate: '2025-08-14',
|
|
51
|
+
priority: 2,
|
|
52
|
+
labels: ['work'],
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
55
|
+
nextCursor: null,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
it('should handle explicit completion date query', async () => {
|
|
59
|
+
mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue({
|
|
60
|
+
items: [],
|
|
61
|
+
nextCursor: 'next-cursor',
|
|
62
|
+
});
|
|
63
|
+
const result = await tasksListCompleted.execute({
|
|
64
|
+
getBy: 'completion',
|
|
65
|
+
limit: 100,
|
|
66
|
+
since: '2025-08-01',
|
|
67
|
+
until: '2025-08-31',
|
|
68
|
+
projectId: 'specific-project-id',
|
|
69
|
+
cursor: 'current-cursor',
|
|
70
|
+
}, mockTodoistApi);
|
|
71
|
+
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
72
|
+
since: '2025-08-01',
|
|
73
|
+
until: '2025-08-31',
|
|
74
|
+
projectId: 'specific-project-id',
|
|
75
|
+
limit: 100,
|
|
76
|
+
cursor: 'current-cursor',
|
|
77
|
+
});
|
|
78
|
+
expect(result).toEqual({ tasks: [], nextCursor: 'next-cursor' });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('getting completed tasks by due date', () => {
|
|
82
|
+
it('should get completed tasks by due date', async () => {
|
|
83
|
+
const mockCompletedTasks = [
|
|
84
|
+
createMockTask({
|
|
85
|
+
id: '8485093750',
|
|
86
|
+
content: 'Task completed by due date',
|
|
87
|
+
description: 'This task was due and completed',
|
|
88
|
+
completedAt: '2024-01-01T00:00:00Z',
|
|
89
|
+
labels: ['urgent'],
|
|
90
|
+
priority: 3,
|
|
91
|
+
url: 'https://todoist.com/showTask?id=8485093750',
|
|
92
|
+
addedAt: '2025-08-13T22:09:58.123456Z',
|
|
93
|
+
due: {
|
|
94
|
+
date: '2025-08-15',
|
|
95
|
+
isRecurring: true,
|
|
96
|
+
lang: 'en',
|
|
97
|
+
string: 'every Monday',
|
|
98
|
+
timezone: null,
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
102
|
+
mockTodoistApi.getCompletedTasksByDueDate.mockResolvedValue({
|
|
103
|
+
items: mockCompletedTasks,
|
|
104
|
+
nextCursor: null,
|
|
105
|
+
});
|
|
106
|
+
const result = await tasksListCompleted.execute({
|
|
107
|
+
getBy: 'due',
|
|
108
|
+
limit: 50,
|
|
109
|
+
since: '2025-08-10',
|
|
110
|
+
until: '2025-08-20',
|
|
111
|
+
}, mockTodoistApi);
|
|
112
|
+
expect(mockTodoistApi.getCompletedTasksByDueDate).toHaveBeenCalledWith({
|
|
113
|
+
since: '2025-08-10',
|
|
114
|
+
until: '2025-08-20',
|
|
115
|
+
limit: 50,
|
|
116
|
+
});
|
|
117
|
+
expect(mockTodoistApi.getCompletedTasksByCompletionDate).not.toHaveBeenCalled();
|
|
118
|
+
expect(result).toEqual({
|
|
119
|
+
tasks: [
|
|
120
|
+
expect.objectContaining({
|
|
121
|
+
id: '8485093750',
|
|
122
|
+
content: 'Task completed by due date',
|
|
123
|
+
description: 'This task was due and completed',
|
|
124
|
+
dueDate: '2025-08-15',
|
|
125
|
+
recurring: 'every Monday',
|
|
126
|
+
priority: 3,
|
|
127
|
+
labels: ['urgent'],
|
|
128
|
+
}),
|
|
129
|
+
],
|
|
130
|
+
nextCursor: null,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('error handling', () => {
|
|
135
|
+
it('should propagate completion date API errors', async () => {
|
|
136
|
+
const apiError = new Error('API Error: Invalid date range');
|
|
137
|
+
mockTodoistApi.getCompletedTasksByCompletionDate.mockRejectedValue(apiError);
|
|
138
|
+
await expect(tasksListCompleted.execute(
|
|
139
|
+
// invalid date range
|
|
140
|
+
{ getBy: 'completion', limit: 50, since: '2025-08-31', until: '2025-08-01' }, mockTodoistApi)).rejects.toThrow('API Error: Invalid date range');
|
|
141
|
+
});
|
|
142
|
+
it('should propagate due date API errors', async () => {
|
|
143
|
+
const apiError = new Error('API Error: Project not found');
|
|
144
|
+
mockTodoistApi.getCompletedTasksByDueDate.mockRejectedValue(apiError);
|
|
145
|
+
await expect(tasksListCompleted.execute({
|
|
146
|
+
getBy: 'due',
|
|
147
|
+
limit: 50,
|
|
148
|
+
since: '2025-08-01',
|
|
149
|
+
until: '2025-08-31',
|
|
150
|
+
projectId: 'non-existent-project',
|
|
151
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-list-for-container.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-list-for-container.test.ts"],"names":[],"mappings":""}
|