@doist/todoist-ai 2.0.0 → 2.0.1
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 +18 -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 +11 -1
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +41 -22
- package/dist/tool-helpers.test.js +35 -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 +160 -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 +161 -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.js +13 -16
- package/dist/tools/tasks-complete-multiple.js +3 -6
- package/dist/tools/tasks-list-by-date.js +12 -15
- package/dist/tools/tasks-list-completed.d.ts +1 -1
- package/dist/tools/tasks-list-completed.js +13 -16
- 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.js +7 -10
- package/dist/tools/tasks-update-one.d.ts +2 -2
- package/dist/tools/tasks-update-one.d.ts.map +1 -1
- package/dist/tools/tasks-update-one.js +22 -15
- package/dist/tools/test-helpers.d.ts +79 -0
- package/dist/tools/test-helpers.d.ts.map +1 -0
- package/dist/tools/test-helpers.js +139 -0
- package/package.json +6 -2
- package/scripts/test-executable.cjs +69 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { tasksOrganizeMultiple } from '../tasks-organize-multiple.js';
|
|
3
|
+
import { TEST_IDS, createMockTask } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
updateTask: jest.fn(),
|
|
7
|
+
moveTasks: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
describe('tasks-organize-multiple tool', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
describe('organizing multiple tasks', () => {
|
|
14
|
+
it('should move multiple tasks to the same destination', async () => {
|
|
15
|
+
const sectionId = '6cfPqr9xgvmgW6J0';
|
|
16
|
+
const mockResponses = [
|
|
17
|
+
createMockTask({ id: '6cPuJm79x4QhMwR4', content: 'First task', sectionId }),
|
|
18
|
+
createMockTask({ id: '6cPHJj2MV4HMj92W', content: 'Second task', sectionId }),
|
|
19
|
+
];
|
|
20
|
+
// Each task should be moved individually to avoid bulk operation issues
|
|
21
|
+
mockTodoistApi.moveTasks
|
|
22
|
+
.mockResolvedValueOnce([mockResponses[0]])
|
|
23
|
+
.mockResolvedValueOnce([mockResponses[1]]);
|
|
24
|
+
const result = await tasksOrganizeMultiple.execute({
|
|
25
|
+
tasks: [
|
|
26
|
+
{ id: '6cPHJm59x4WhMwR4', sectionId },
|
|
27
|
+
{ id: '6cPHJj2MV4HMj92W', sectionId },
|
|
28
|
+
],
|
|
29
|
+
}, mockTodoistApi);
|
|
30
|
+
// Should call moveTasks twice, once for each task individually
|
|
31
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledTimes(2);
|
|
32
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenNthCalledWith(1, ['6cPHJm59x4WhMwR4'], {
|
|
33
|
+
sectionId,
|
|
34
|
+
});
|
|
35
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenNthCalledWith(2, ['6cPHJj2MV4HMj92W'], {
|
|
36
|
+
sectionId,
|
|
37
|
+
});
|
|
38
|
+
expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
|
|
39
|
+
expect(result).toEqual(mockResponses);
|
|
40
|
+
});
|
|
41
|
+
it('should move multiple tasks with different destinations', async () => {
|
|
42
|
+
const { TASK_1, TASK_2, TASK_3 } = TEST_IDS;
|
|
43
|
+
const mockResponses = [
|
|
44
|
+
createMockTask({ id: TASK_1, content: 'Task 1', projectId: 'new-project-id' }),
|
|
45
|
+
createMockTask({ id: TASK_2, content: 'Task 2', sectionId: 'new-section-id' }),
|
|
46
|
+
createMockTask({ id: TASK_3, content: 'Task 3', parentId: 'parent-task-123' }),
|
|
47
|
+
];
|
|
48
|
+
// Each task should be moved individually
|
|
49
|
+
mockTodoistApi.moveTasks
|
|
50
|
+
.mockResolvedValueOnce([mockResponses[0]])
|
|
51
|
+
.mockResolvedValueOnce([mockResponses[1]])
|
|
52
|
+
.mockResolvedValueOnce([mockResponses[2]]);
|
|
53
|
+
const result = await tasksOrganizeMultiple.execute({
|
|
54
|
+
tasks: [
|
|
55
|
+
{ id: '8485093748', projectId: 'new-project-id' },
|
|
56
|
+
{ id: '8485093749', sectionId: 'new-section-id' },
|
|
57
|
+
{ id: '8485093750', parentId: 'parent-task-123' },
|
|
58
|
+
],
|
|
59
|
+
}, mockTodoistApi);
|
|
60
|
+
// Verify API was called correctly - 3 individual move calls
|
|
61
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledTimes(3);
|
|
62
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenNthCalledWith(1, ['8485093748'], {
|
|
63
|
+
projectId: 'new-project-id',
|
|
64
|
+
});
|
|
65
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenNthCalledWith(2, ['8485093749'], {
|
|
66
|
+
sectionId: 'new-section-id',
|
|
67
|
+
});
|
|
68
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenNthCalledWith(3, ['8485093750'], {
|
|
69
|
+
parentId: 'parent-task-123',
|
|
70
|
+
});
|
|
71
|
+
expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
|
|
72
|
+
// Verify results are returned in the correct order
|
|
73
|
+
expect(result).toEqual(mockResponses);
|
|
74
|
+
});
|
|
75
|
+
it('should handle single task organization', async () => {
|
|
76
|
+
const mockTaskResponse = createMockTask({
|
|
77
|
+
id: '8485093751',
|
|
78
|
+
content: 'Single task update',
|
|
79
|
+
sectionId: 'target-section',
|
|
80
|
+
url: 'https://todoist.com/showTask?id=8485093751',
|
|
81
|
+
addedAt: '2025-08-13T22:09:59.123456Z',
|
|
82
|
+
});
|
|
83
|
+
mockTodoistApi.moveTasks.mockResolvedValue([mockTaskResponse]);
|
|
84
|
+
const result = await tasksOrganizeMultiple.execute({ tasks: [{ id: '8485093751', sectionId: 'target-section' }] }, mockTodoistApi);
|
|
85
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledWith(['8485093751'], {
|
|
87
|
+
sectionId: 'target-section',
|
|
88
|
+
});
|
|
89
|
+
expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
|
|
90
|
+
expect(result).toEqual([mockTaskResponse]);
|
|
91
|
+
});
|
|
92
|
+
it('should handle complex reorganization scenario', async () => {
|
|
93
|
+
// Simulate moving tasks to different destinations (one move param per task)
|
|
94
|
+
const mockResponses = [
|
|
95
|
+
createMockTask({
|
|
96
|
+
id: 'task-1',
|
|
97
|
+
content: 'Task moved to new project',
|
|
98
|
+
projectId: 'project-new',
|
|
99
|
+
url: 'https://todoist.com/showTask?id=task-1',
|
|
100
|
+
addedAt: '2025-08-13T22:10:00.123456Z',
|
|
101
|
+
}),
|
|
102
|
+
createMockTask({
|
|
103
|
+
id: 'task-2',
|
|
104
|
+
content: 'Task made into subtask',
|
|
105
|
+
parentId: 'task-1',
|
|
106
|
+
url: 'https://todoist.com/showTask?id=task-2',
|
|
107
|
+
addedAt: '2025-08-13T22:10:01.123456Z',
|
|
108
|
+
}),
|
|
109
|
+
createMockTask({
|
|
110
|
+
id: 'task-3',
|
|
111
|
+
content: 'Task moved to section',
|
|
112
|
+
sectionId: 'section-new',
|
|
113
|
+
url: 'https://todoist.com/showTask?id=task-3',
|
|
114
|
+
addedAt: '2025-08-13T22:10:02.123456Z',
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
117
|
+
// Each task should be moved individually
|
|
118
|
+
mockTodoistApi.moveTasks
|
|
119
|
+
.mockResolvedValueOnce([mockResponses[0]])
|
|
120
|
+
.mockResolvedValueOnce([mockResponses[1]])
|
|
121
|
+
.mockResolvedValueOnce([mockResponses[2]]);
|
|
122
|
+
const result = await tasksOrganizeMultiple.execute({
|
|
123
|
+
tasks: [
|
|
124
|
+
{ id: 'task-1', projectId: 'project-new' },
|
|
125
|
+
{ id: 'task-2', parentId: 'task-1' },
|
|
126
|
+
{ id: 'task-3', sectionId: 'section-new' },
|
|
127
|
+
],
|
|
128
|
+
}, mockTodoistApi);
|
|
129
|
+
// Verify API was called correctly - 3 individual move calls
|
|
130
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledTimes(3);
|
|
131
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenNthCalledWith(1, ['task-1'], {
|
|
132
|
+
projectId: 'project-new',
|
|
133
|
+
});
|
|
134
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenNthCalledWith(2, ['task-2'], {
|
|
135
|
+
parentId: 'task-1',
|
|
136
|
+
});
|
|
137
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenNthCalledWith(3, ['task-3'], {
|
|
138
|
+
sectionId: 'section-new',
|
|
139
|
+
});
|
|
140
|
+
expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
|
|
141
|
+
expect(result).toEqual(mockResponses);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('order management', () => {
|
|
145
|
+
it('should skip tasks with only order changes (no move operations)', async () => {
|
|
146
|
+
const result = await tasksOrganizeMultiple.execute({
|
|
147
|
+
tasks: [
|
|
148
|
+
{ id: 'task-a', order: 3 }, // Only order, no move params
|
|
149
|
+
{ id: 'task-b', order: 1 }, // Only order, no move params
|
|
150
|
+
{ id: 'task-c', order: 2 }, // Only order, no move params
|
|
151
|
+
],
|
|
152
|
+
}, mockTodoistApi);
|
|
153
|
+
// No API calls should be made since only order is specified (no projectId/sectionId/parentId)
|
|
154
|
+
expect(mockTodoistApi.moveTasks).not.toHaveBeenCalled();
|
|
155
|
+
expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
|
|
156
|
+
// Returns empty array since no moves were processed
|
|
157
|
+
expect(result).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('partial updates', () => {
|
|
161
|
+
it('should handle move operations with single parameters', async () => {
|
|
162
|
+
const mockResponse = createMockTask({
|
|
163
|
+
id: '8485093752',
|
|
164
|
+
content: 'Minimal update task',
|
|
165
|
+
projectId: 'new-project-only',
|
|
166
|
+
url: 'https://todoist.com/showTask?id=8485093752',
|
|
167
|
+
addedAt: '2025-08-13T22:10:07.123456Z',
|
|
168
|
+
});
|
|
169
|
+
mockTodoistApi.moveTasks.mockResolvedValue([mockResponse]);
|
|
170
|
+
const result = await tasksOrganizeMultiple.execute({
|
|
171
|
+
tasks: [
|
|
172
|
+
{
|
|
173
|
+
id: '8485093752',
|
|
174
|
+
projectId: 'new-project-only',
|
|
175
|
+
// Only updating projectId (move operation)
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
}, mockTodoistApi);
|
|
179
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledWith(['8485093752'], {
|
|
180
|
+
projectId: 'new-project-only',
|
|
181
|
+
});
|
|
182
|
+
expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
|
|
183
|
+
expect(result).toEqual([mockResponse]);
|
|
184
|
+
});
|
|
185
|
+
it('should handle empty updates (only id provided)', async () => {
|
|
186
|
+
const result = await tasksOrganizeMultiple.execute({ tasks: [{ id: '8485093753' }] }, mockTodoistApi);
|
|
187
|
+
// No API calls should be made since no move parameters are provided
|
|
188
|
+
expect(mockTodoistApi.moveTasks).not.toHaveBeenCalled();
|
|
189
|
+
expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
|
|
190
|
+
// Returns empty array since no moves were processed
|
|
191
|
+
expect(result).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('error handling', () => {
|
|
195
|
+
it('should throw error when task has multiple move parameters', async () => {
|
|
196
|
+
await expect(tasksOrganizeMultiple.execute({
|
|
197
|
+
tasks: [
|
|
198
|
+
{ id: 'task-1', projectId: 'new-project', sectionId: 'new-section' },
|
|
199
|
+
],
|
|
200
|
+
}, mockTodoistApi)).rejects.toThrow('Task task-1: Only one of projectId, sectionId, or parentId can be specified at a time');
|
|
201
|
+
});
|
|
202
|
+
it('should propagate API errors for individual task moves', async () => {
|
|
203
|
+
const apiError = new Error('API Error: Task not found');
|
|
204
|
+
mockTodoistApi.moveTasks.mockRejectedValue(apiError);
|
|
205
|
+
await expect(tasksOrganizeMultiple.execute({ tasks: [{ id: 'non-existent-task', projectId: 'some-project' }] }, mockTodoistApi)).rejects.toThrow('API Error: Task not found');
|
|
206
|
+
});
|
|
207
|
+
it('should fail fast on first error (not continue with remaining tasks)', async () => {
|
|
208
|
+
const apiError = new Error('API Error: Invalid project ID');
|
|
209
|
+
mockTodoistApi.moveTasks.mockRejectedValue(apiError);
|
|
210
|
+
await expect(tasksOrganizeMultiple.execute({
|
|
211
|
+
tasks: [
|
|
212
|
+
{ id: 'task-1', projectId: 'invalid-project' },
|
|
213
|
+
{ id: 'task-2', projectId: 'valid-project' },
|
|
214
|
+
],
|
|
215
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Invalid project ID');
|
|
216
|
+
// Should only attempt the first move
|
|
217
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledTimes(1);
|
|
218
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledWith(['task-1'], {
|
|
219
|
+
projectId: 'invalid-project',
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
it('should handle validation errors', async () => {
|
|
223
|
+
const validationError = new Error('API Error: Invalid section ID');
|
|
224
|
+
mockTodoistApi.moveTasks.mockRejectedValue(validationError);
|
|
225
|
+
await expect(tasksOrganizeMultiple.execute({ tasks: [{ id: 'task-1', sectionId: 'invalid-section-format' }] }, mockTodoistApi)).rejects.toThrow('API Error: Invalid section ID');
|
|
226
|
+
});
|
|
227
|
+
it('should handle permission errors', async () => {
|
|
228
|
+
const permissionError = new Error('API Error: Insufficient permissions to move task');
|
|
229
|
+
mockTodoistApi.moveTasks.mockRejectedValue(permissionError);
|
|
230
|
+
await expect(tasksOrganizeMultiple.execute({ tasks: [{ id: 'restricted-task', projectId: 'restricted-project' }] }, mockTodoistApi)).rejects.toThrow('API Error: Insufficient permissions to move task');
|
|
231
|
+
});
|
|
232
|
+
it('should handle circular parent dependency errors', async () => {
|
|
233
|
+
const circularError = new Error('API Error: Circular dependency detected');
|
|
234
|
+
mockTodoistApi.moveTasks.mockRejectedValue(circularError);
|
|
235
|
+
await expect(tasksOrganizeMultiple.execute({
|
|
236
|
+
tasks: [
|
|
237
|
+
{
|
|
238
|
+
id: 'task-parent',
|
|
239
|
+
parentId: 'task-child', // This would create a circular dependency
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Circular dependency detected');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-search.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-search.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
+
import { tasksSearch } from '../tasks-search.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-search, but needed for type)
|
|
11
|
+
const mockTodoistApi = {};
|
|
12
|
+
describe('tasks-search tool', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
describe('searching tasks', () => {
|
|
17
|
+
it('should search tasks and return results', async () => {
|
|
18
|
+
const mockTasks = [
|
|
19
|
+
createMappedTask({
|
|
20
|
+
id: TEST_IDS.TASK_1,
|
|
21
|
+
content: 'Task containing search term',
|
|
22
|
+
description: 'Description with more details',
|
|
23
|
+
labels: ['work'],
|
|
24
|
+
}),
|
|
25
|
+
createMappedTask({
|
|
26
|
+
id: TEST_IDS.TASK_2,
|
|
27
|
+
content: 'Another matching task',
|
|
28
|
+
priority: 2,
|
|
29
|
+
sectionId: TEST_IDS.SECTION_1,
|
|
30
|
+
}),
|
|
31
|
+
];
|
|
32
|
+
const mockResponse = { tasks: mockTasks, nextCursor: 'cursor-for-next-page' };
|
|
33
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
34
|
+
const result = await tasksSearch.execute({
|
|
35
|
+
searchText: 'important meeting',
|
|
36
|
+
limit: 10,
|
|
37
|
+
}, mockTodoistApi);
|
|
38
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
39
|
+
client: mockTodoistApi,
|
|
40
|
+
query: 'search: important meeting',
|
|
41
|
+
cursor: undefined,
|
|
42
|
+
limit: 10,
|
|
43
|
+
});
|
|
44
|
+
expect(result).toEqual(mockResponse);
|
|
45
|
+
});
|
|
46
|
+
it.each([
|
|
47
|
+
{
|
|
48
|
+
name: 'custom limit',
|
|
49
|
+
params: { searchText: 'project update', limit: 5 },
|
|
50
|
+
expectedQuery: 'search: project update',
|
|
51
|
+
expectedLimit: 5,
|
|
52
|
+
expectedCursor: undefined,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'pagination cursor',
|
|
56
|
+
params: { searchText: 'follow up', limit: 20, cursor: 'cursor-from-first-page' },
|
|
57
|
+
expectedQuery: 'search: follow up',
|
|
58
|
+
expectedLimit: 20,
|
|
59
|
+
expectedCursor: 'cursor-from-first-page',
|
|
60
|
+
},
|
|
61
|
+
])('should handle $name', async ({ params, expectedQuery, expectedLimit, expectedCursor }) => {
|
|
62
|
+
const mockTask = createMappedTask({ content: 'Test result' });
|
|
63
|
+
const mockResponse = { tasks: [mockTask], nextCursor: null };
|
|
64
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
65
|
+
const result = await tasksSearch.execute(params, mockTodoistApi);
|
|
66
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
67
|
+
client: mockTodoistApi,
|
|
68
|
+
query: expectedQuery,
|
|
69
|
+
cursor: expectedCursor,
|
|
70
|
+
limit: expectedLimit,
|
|
71
|
+
});
|
|
72
|
+
expect(result).toEqual(mockResponse);
|
|
73
|
+
});
|
|
74
|
+
it.each([
|
|
75
|
+
{ searchText: '@work #urgent "exact phrase"', description: 'special characters' },
|
|
76
|
+
{ searchText: 'nonexistent keyword', description: 'empty results' },
|
|
77
|
+
])('should handle search with $description', async ({ searchText }) => {
|
|
78
|
+
const mockResponse = { tasks: [], nextCursor: null };
|
|
79
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
80
|
+
const result = await tasksSearch.execute({ searchText, limit: 10 }, mockTodoistApi);
|
|
81
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
82
|
+
client: mockTodoistApi,
|
|
83
|
+
query: `search: ${searchText}`,
|
|
84
|
+
cursor: undefined,
|
|
85
|
+
limit: 10,
|
|
86
|
+
});
|
|
87
|
+
expect(result).toEqual(mockResponse);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('error handling', () => {
|
|
91
|
+
it.each([
|
|
92
|
+
{ error: 'Invalid filter query: search: ', params: { searchText: '', limit: 10 } },
|
|
93
|
+
{
|
|
94
|
+
error: TEST_ERRORS.API_RATE_LIMIT,
|
|
95
|
+
params: { searchText: 'any search term', limit: 10 },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
error: TEST_ERRORS.INVALID_CURSOR,
|
|
99
|
+
params: { searchText: 'test', cursor: 'invalid-cursor-format', limit: 10 },
|
|
100
|
+
},
|
|
101
|
+
])('should propagate $error', async ({ error, params }) => {
|
|
102
|
+
mockGetTasksByFilter.mockRejectedValue(new Error(error));
|
|
103
|
+
await expect(tasksSearch.execute(params, mockTodoistApi)).rejects.toThrow(error);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-update-one.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-update-one.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { tasksUpdateOne } from '../tasks-update-one.js';
|
|
3
|
+
import { createMockTask } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
updateTask: jest.fn(),
|
|
7
|
+
moveTasks: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
describe('tasks-update-one tool', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
describe('updating task properties', () => {
|
|
14
|
+
it('should update task content and description', async () => {
|
|
15
|
+
// Mock API response extracted from recordings (Task type)
|
|
16
|
+
const mockApiResponse = createMockTask({
|
|
17
|
+
id: '8485093748',
|
|
18
|
+
content: 'Updated task content',
|
|
19
|
+
description: 'Updated task description',
|
|
20
|
+
url: 'https://todoist.com/showTask?id=8485093748',
|
|
21
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
22
|
+
});
|
|
23
|
+
mockTodoistApi.updateTask.mockResolvedValue(mockApiResponse);
|
|
24
|
+
const result = await tasksUpdateOne.execute({
|
|
25
|
+
id: '8485093748',
|
|
26
|
+
content: 'Updated task content',
|
|
27
|
+
description: 'Updated task description',
|
|
28
|
+
}, mockTodoistApi);
|
|
29
|
+
// Verify API was called correctly
|
|
30
|
+
expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093748', {
|
|
31
|
+
content: 'Updated task content',
|
|
32
|
+
description: 'Updated task description',
|
|
33
|
+
});
|
|
34
|
+
// Verify result matches API response
|
|
35
|
+
expect(result).toEqual(mockApiResponse);
|
|
36
|
+
});
|
|
37
|
+
it('should update task priority and due date', async () => {
|
|
38
|
+
const mockApiResponse = createMockTask({
|
|
39
|
+
id: '8485093749',
|
|
40
|
+
content: 'Original task content',
|
|
41
|
+
labels: ['urgent'],
|
|
42
|
+
priority: 3,
|
|
43
|
+
url: 'https://todoist.com/showTask?id=8485093749',
|
|
44
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
45
|
+
due: {
|
|
46
|
+
date: '2025-08-20',
|
|
47
|
+
isRecurring: false,
|
|
48
|
+
lang: 'en',
|
|
49
|
+
string: 'Aug 20',
|
|
50
|
+
timezone: null,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
mockTodoistApi.updateTask.mockResolvedValue(mockApiResponse);
|
|
54
|
+
const result = await tasksUpdateOne.execute({ id: '8485093749', priority: 3, dueString: 'Aug 20' }, mockTodoistApi);
|
|
55
|
+
expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093749', {
|
|
56
|
+
priority: 3,
|
|
57
|
+
dueString: 'Aug 20',
|
|
58
|
+
});
|
|
59
|
+
expect(result).toEqual(mockApiResponse);
|
|
60
|
+
});
|
|
61
|
+
it('should move task to different project', async () => {
|
|
62
|
+
const mockApiResponse = createMockTask({
|
|
63
|
+
id: '8485093750',
|
|
64
|
+
content: 'Task to move',
|
|
65
|
+
projectId: 'new-project-id',
|
|
66
|
+
url: 'https://todoist.com/showTask?id=8485093750',
|
|
67
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
68
|
+
});
|
|
69
|
+
mockTodoistApi.moveTasks.mockResolvedValue([mockApiResponse]);
|
|
70
|
+
const result = await tasksUpdateOne.execute({ id: '8485093750', projectId: 'new-project-id' }, mockTodoistApi);
|
|
71
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledWith(['8485093750'], {
|
|
72
|
+
projectId: 'new-project-id',
|
|
73
|
+
});
|
|
74
|
+
expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
|
|
75
|
+
expect(result).toEqual(mockApiResponse);
|
|
76
|
+
});
|
|
77
|
+
it('should update task parent (create subtask relationship)', async () => {
|
|
78
|
+
const mockApiResponse = createMockTask({
|
|
79
|
+
id: '8485093751',
|
|
80
|
+
content: 'Subtask content',
|
|
81
|
+
parentId: 'parent-task-123',
|
|
82
|
+
url: 'https://todoist.com/showTask?id=8485093751',
|
|
83
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
84
|
+
});
|
|
85
|
+
mockTodoistApi.moveTasks.mockResolvedValue([mockApiResponse]);
|
|
86
|
+
const result = await tasksUpdateOne.execute({ id: '8485093751', parentId: 'parent-task-123' }, mockTodoistApi);
|
|
87
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledWith(['8485093751'], {
|
|
88
|
+
parentId: 'parent-task-123',
|
|
89
|
+
});
|
|
90
|
+
expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
|
|
91
|
+
expect(result).toEqual(mockApiResponse);
|
|
92
|
+
});
|
|
93
|
+
it('should move task and update properties at once', async () => {
|
|
94
|
+
const movedTask = createMockTask({
|
|
95
|
+
id: '8485093752',
|
|
96
|
+
content: 'Task to move',
|
|
97
|
+
projectId: 'different-project-id',
|
|
98
|
+
});
|
|
99
|
+
const updatedTask = createMockTask({
|
|
100
|
+
id: '8485093752',
|
|
101
|
+
content: 'Completely updated task',
|
|
102
|
+
description: 'New description with details',
|
|
103
|
+
priority: 4,
|
|
104
|
+
projectId: 'different-project-id',
|
|
105
|
+
url: 'https://todoist.com/showTask?id=8485093752',
|
|
106
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
107
|
+
due: {
|
|
108
|
+
date: '2025-08-25',
|
|
109
|
+
isRecurring: true,
|
|
110
|
+
lang: 'en',
|
|
111
|
+
string: 'every Friday',
|
|
112
|
+
timezone: null,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
mockTodoistApi.moveTasks.mockResolvedValue([movedTask]);
|
|
116
|
+
mockTodoistApi.updateTask.mockResolvedValue(updatedTask);
|
|
117
|
+
const result = await tasksUpdateOne.execute({
|
|
118
|
+
id: '8485093752',
|
|
119
|
+
content: 'Completely updated task',
|
|
120
|
+
description: 'New description with details',
|
|
121
|
+
priority: 4,
|
|
122
|
+
dueString: 'every Friday',
|
|
123
|
+
projectId: 'different-project-id',
|
|
124
|
+
}, mockTodoistApi);
|
|
125
|
+
// Should call moveTasks first for the projectId
|
|
126
|
+
expect(mockTodoistApi.moveTasks).toHaveBeenCalledWith(['8485093752'], {
|
|
127
|
+
projectId: 'different-project-id',
|
|
128
|
+
});
|
|
129
|
+
// Then call updateTask for the other properties
|
|
130
|
+
expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093752', {
|
|
131
|
+
content: 'Completely updated task',
|
|
132
|
+
description: 'New description with details',
|
|
133
|
+
priority: 4,
|
|
134
|
+
dueString: 'every Friday',
|
|
135
|
+
});
|
|
136
|
+
expect(result).toEqual(updatedTask);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('error handling', () => {
|
|
140
|
+
it('should throw error when multiple move parameters are provided', async () => {
|
|
141
|
+
await expect(tasksUpdateOne.execute({ id: '8485093748', projectId: 'new-project', sectionId: 'new-section' }, mockTodoistApi)).rejects.toThrow('Only one of projectId, sectionId, or parentId can be specified at a time. ' +
|
|
142
|
+
'The Todoist API requires exactly one destination for move operations.');
|
|
143
|
+
});
|
|
144
|
+
it('should throw error when all three move parameters are provided', async () => {
|
|
145
|
+
await expect(tasksUpdateOne.execute({ id: '8485093748', projectId: 'p1', sectionId: 's1', parentId: 't1' }, mockTodoistApi)).rejects.toThrow('Only one of projectId, sectionId, or parentId can be specified at a time');
|
|
146
|
+
});
|
|
147
|
+
it.each([
|
|
148
|
+
{
|
|
149
|
+
error: 'API Error: Task not found',
|
|
150
|
+
params: { id: 'non-existent-task', content: 'Updated content' },
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
error: 'API Error: Invalid priority value',
|
|
154
|
+
params: { id: '8485093748', priority: 5 },
|
|
155
|
+
},
|
|
156
|
+
])('should propagate $error', async ({ error, params }) => {
|
|
157
|
+
mockTodoistApi.updateTask.mockRejectedValue(new Error(error));
|
|
158
|
+
await expect(tasksUpdateOne.execute(params, mockTodoistApi)).rejects.toThrow(error);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
package/dist/tools/delete-one.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.deleteOne = void 0;
|
|
4
|
-
const zod_1 = require("zod");
|
|
1
|
+
import { z } from 'zod';
|
|
5
2
|
const ArgsSchema = {
|
|
6
|
-
type:
|
|
7
|
-
id:
|
|
3
|
+
type: z.enum(['project', 'section', 'task']).describe('The type of entity to delete.'),
|
|
4
|
+
id: z.string().min(1).describe('The ID of the entity to delete.'),
|
|
8
5
|
};
|
|
9
6
|
const deleteOne = {
|
|
10
7
|
name: 'delete-one',
|
|
@@ -25,4 +22,4 @@ const deleteOne = {
|
|
|
25
22
|
return { success: true };
|
|
26
23
|
},
|
|
27
24
|
};
|
|
28
|
-
|
|
25
|
+
export { deleteOne };
|
package/dist/tools/overview.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.overview = void 0;
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
const tool_helpers_1 = require("../tool-helpers");
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { isPersonalProject, mapTask } from '../tool-helpers.js';
|
|
6
3
|
const ArgsSchema = {
|
|
7
|
-
projectId:
|
|
4
|
+
projectId: z
|
|
8
5
|
.string()
|
|
9
6
|
.min(1)
|
|
10
7
|
.optional()
|
|
@@ -25,7 +22,7 @@ function buildProjectTree(projects) {
|
|
|
25
22
|
const current = byId[p.id];
|
|
26
23
|
if (!current)
|
|
27
24
|
continue;
|
|
28
|
-
if (
|
|
25
|
+
if (isPersonalProject(p) && p.parentId) {
|
|
29
26
|
const parent = byId[p.parentId];
|
|
30
27
|
if (parent) {
|
|
31
28
|
parent.children.push(current);
|
|
@@ -113,7 +110,7 @@ async function getAllTasksForProject(client, projectId) {
|
|
|
113
110
|
limit: 50,
|
|
114
111
|
cursor: cursor ?? undefined,
|
|
115
112
|
});
|
|
116
|
-
allTasks = allTasks.concat(results.map(
|
|
113
|
+
allTasks = allTasks.concat(results.map(mapTask));
|
|
117
114
|
cursor = nextCursor ?? undefined;
|
|
118
115
|
} while (cursor);
|
|
119
116
|
return allTasks;
|
|
@@ -125,8 +122,8 @@ async function getProjectSections(client, projectId) {
|
|
|
125
122
|
// Account overview implementation
|
|
126
123
|
async function generateAccountOverview(client) {
|
|
127
124
|
const { results: projects } = await client.getProjects({});
|
|
128
|
-
const inbox = projects.find((p) =>
|
|
129
|
-
const nonInbox = projects.filter((p) => !
|
|
125
|
+
const inbox = projects.find((p) => isPersonalProject(p) && p.inboxProject === true);
|
|
126
|
+
const nonInbox = projects.filter((p) => !isPersonalProject(p) || p.inboxProject !== true);
|
|
130
127
|
const tree = buildProjectTree(nonInbox);
|
|
131
128
|
const allProjectIds = projects.map((p) => p.id);
|
|
132
129
|
const sectionsByProject = await getSectionsByProject(client, allProjectIds);
|
|
@@ -202,4 +199,4 @@ const overview = {
|
|
|
202
199
|
return await generateAccountOverview(client);
|
|
203
200
|
},
|
|
204
201
|
};
|
|
205
|
-
|
|
202
|
+
export { overview };
|
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.projectsList = void 0;
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
const tool_helpers_1 = require("../tool-helpers");
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { mapProject } from '../tool-helpers.js';
|
|
6
3
|
const ArgsSchema = {
|
|
7
|
-
search:
|
|
4
|
+
search: z
|
|
8
5
|
.string()
|
|
9
6
|
.optional()
|
|
10
7
|
.describe('Search for a project by name (partial and case insensitive match). If omitted, all projects are returned.'),
|
|
11
|
-
limit:
|
|
8
|
+
limit: z
|
|
12
9
|
.number()
|
|
13
10
|
.int()
|
|
14
11
|
.min(1)
|
|
15
12
|
.max(100)
|
|
16
13
|
.default(50)
|
|
17
14
|
.describe('The maximum number of projects to return.'),
|
|
18
|
-
cursor:
|
|
15
|
+
cursor: z
|
|
19
16
|
.string()
|
|
20
17
|
.optional()
|
|
21
18
|
.describe('The cursor to get the next page of projects (cursor is obtained from the previous call to this tool, with the same parameters).'),
|
|
@@ -34,9 +31,9 @@ const projectsList = {
|
|
|
34
31
|
? results.filter((project) => project.name.toLowerCase().includes(searchLower))
|
|
35
32
|
: results;
|
|
36
33
|
return {
|
|
37
|
-
projects: filtered.map(
|
|
34
|
+
projects: filtered.map(mapProject),
|
|
38
35
|
nextCursor,
|
|
39
36
|
};
|
|
40
37
|
},
|
|
41
38
|
};
|
|
42
|
-
|
|
39
|
+
export { projectsList };
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.projectsManage = void 0;
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
const tool_helpers_1 = require("../tool-helpers");
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { mapProject } from '../tool-helpers.js';
|
|
6
3
|
const ArgsSchema = {
|
|
7
|
-
id:
|
|
4
|
+
id: z
|
|
8
5
|
.string()
|
|
9
6
|
.min(1)
|
|
10
7
|
.optional()
|
|
11
8
|
.describe('The ID of the project to update. If provided, updates the project. If omitted, creates a new project.'),
|
|
12
|
-
name:
|
|
9
|
+
name: z.string().min(1).describe('The name of the project.'),
|
|
13
10
|
};
|
|
14
11
|
const projectsManage = {
|
|
15
12
|
name: 'projects-manage',
|
|
@@ -23,7 +20,7 @@ const projectsManage = {
|
|
|
23
20
|
}
|
|
24
21
|
// Create new project
|
|
25
22
|
const project = await client.addProject({ name: args.name });
|
|
26
|
-
return
|
|
23
|
+
return mapProject(project);
|
|
27
24
|
},
|
|
28
25
|
};
|
|
29
|
-
|
|
26
|
+
export { projectsManage };
|