@doist/todoist-ai 4.4.0 → 4.6.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.
Files changed (80) hide show
  1. package/dist/index.d.ts +168 -35
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +24 -16
  4. package/dist/mcp-helpers.d.ts.map +1 -1
  5. package/dist/mcp-helpers.js +1 -1
  6. package/dist/mcp-server.d.ts.map +1 -1
  7. package/dist/mcp-server.js +78 -17
  8. package/dist/tool-helpers.d.ts +4 -0
  9. package/dist/tool-helpers.d.ts.map +1 -1
  10. package/dist/tool-helpers.js +2 -0
  11. package/dist/tools/__tests__/add-projects.test.js +1 -1
  12. package/dist/tools/__tests__/add-sections.test.js +1 -1
  13. package/dist/tools/__tests__/add-tasks.test.js +182 -13
  14. package/dist/tools/__tests__/assignment-integration.test.d.ts +2 -0
  15. package/dist/tools/__tests__/assignment-integration.test.d.ts.map +1 -0
  16. package/dist/tools/__tests__/assignment-integration.test.js +415 -0
  17. package/dist/tools/__tests__/find-projects.test.js +1 -1
  18. package/dist/tools/__tests__/find-sections.test.js +1 -1
  19. package/dist/tools/__tests__/find-tasks-by-date.test.js +1 -1
  20. package/dist/tools/__tests__/find-tasks.test.js +3 -3
  21. package/dist/tools/__tests__/get-overview.test.js +1 -1
  22. package/dist/tools/__tests__/update-tasks.test.js +82 -6
  23. package/dist/tools/__tests__/user-info.test.js +1 -1
  24. package/dist/tools/add-comments.d.ts +3 -3
  25. package/dist/tools/add-comments.d.ts.map +1 -1
  26. package/dist/tools/add-comments.js +1 -1
  27. package/dist/tools/add-projects.d.ts.map +1 -1
  28. package/dist/tools/add-projects.js +1 -1
  29. package/dist/tools/add-sections.d.ts.map +1 -1
  30. package/dist/tools/add-sections.js +1 -1
  31. package/dist/tools/add-tasks.d.ts +17 -7
  32. package/dist/tools/add-tasks.d.ts.map +1 -1
  33. package/dist/tools/add-tasks.js +51 -4
  34. package/dist/tools/find-comments.d.ts +2 -2
  35. package/dist/tools/find-completed-tasks.d.ts +4 -2
  36. package/dist/tools/find-completed-tasks.d.ts.map +1 -1
  37. package/dist/tools/find-completed-tasks.js +2 -2
  38. package/dist/tools/find-project-collaborators.d.ts +64 -0
  39. package/dist/tools/find-project-collaborators.d.ts.map +1 -0
  40. package/dist/tools/find-project-collaborators.js +151 -0
  41. package/dist/tools/find-tasks-by-date.d.ts +2 -0
  42. package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
  43. package/dist/tools/find-tasks-by-date.js +2 -2
  44. package/dist/tools/find-tasks.d.ts +7 -2
  45. package/dist/tools/find-tasks.d.ts.map +1 -1
  46. package/dist/tools/find-tasks.js +128 -33
  47. package/dist/tools/get-overview.d.ts +2 -2
  48. package/dist/tools/get-overview.d.ts.map +1 -1
  49. package/dist/tools/get-overview.js +1 -1
  50. package/dist/tools/manage-assignments.d.ts +52 -0
  51. package/dist/tools/manage-assignments.d.ts.map +1 -0
  52. package/dist/tools/manage-assignments.js +337 -0
  53. package/dist/tools/update-comments.d.ts.map +1 -1
  54. package/dist/tools/update-comments.js +1 -1
  55. package/dist/tools/update-sections.d.ts.map +1 -1
  56. package/dist/tools/update-sections.js +1 -1
  57. package/dist/tools/update-tasks.d.ts +17 -7
  58. package/dist/tools/update-tasks.d.ts.map +1 -1
  59. package/dist/tools/update-tasks.js +40 -10
  60. package/dist/utils/assignment-validator.d.ts +69 -0
  61. package/dist/utils/assignment-validator.d.ts.map +1 -0
  62. package/dist/utils/assignment-validator.js +253 -0
  63. package/dist/utils/duration-parser.d.ts +2 -2
  64. package/dist/utils/duration-parser.d.ts.map +1 -1
  65. package/dist/utils/priorities.d.ts +8 -0
  66. package/dist/utils/priorities.d.ts.map +1 -0
  67. package/dist/utils/priorities.js +15 -0
  68. package/dist/utils/response-builders.d.ts +2 -2
  69. package/dist/utils/response-builders.d.ts.map +1 -1
  70. package/dist/utils/response-builders.js +8 -1
  71. package/dist/utils/test-helpers.d.ts +2 -0
  72. package/dist/utils/test-helpers.d.ts.map +1 -1
  73. package/dist/utils/test-helpers.js +2 -0
  74. package/dist/utils/tool-names.d.ts +2 -0
  75. package/dist/utils/tool-names.d.ts.map +1 -1
  76. package/dist/utils/tool-names.js +3 -0
  77. package/dist/utils/user-resolver.d.ts +39 -0
  78. package/dist/utils/user-resolver.d.ts.map +1 -0
  79. package/dist/utils/user-resolver.js +179 -0
  80. package/package.json +6 -6
@@ -0,0 +1,415 @@
1
+ import { createMockProject, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
2
+ import { addTasks } from '../add-tasks.js';
3
+ import { findProjectCollaborators } from '../find-project-collaborators.js';
4
+ import { manageAssignments } from '../manage-assignments.js';
5
+ import { updateTasks } from '../update-tasks.js';
6
+ // Mock the assignment validator
7
+ jest.mock('../../utils/assignment-validator.js', () => ({
8
+ assignmentValidator: {
9
+ validateTaskCreationAssignment: jest.fn(),
10
+ validateTaskUpdateAssignment: jest.fn(),
11
+ validateBulkAssignment: jest.fn(),
12
+ },
13
+ }));
14
+ // Mock the user resolver
15
+ jest.mock('../../utils/user-resolver.js', () => ({
16
+ userResolver: {
17
+ resolveUser: jest.fn(),
18
+ getProjectCollaborators: jest.fn(),
19
+ },
20
+ }));
21
+ describe('Assignment Integration Tests', () => {
22
+ let mockTodoistApi;
23
+ const mockValidUser = {
24
+ userId: 'user-123',
25
+ name: 'John Doe',
26
+ email: 'john@example.com',
27
+ };
28
+ const mockTask = {
29
+ id: 'task-123',
30
+ content: 'Test task',
31
+ projectId: 'project-123',
32
+ sectionId: null,
33
+ parentId: null,
34
+ priority: 1,
35
+ labels: [],
36
+ description: '',
37
+ url: 'https://todoist.com/showTask?id=task-123',
38
+ noteCount: 0,
39
+ addedByUid: 'creator-123',
40
+ addedAt: new Date().toISOString(),
41
+ deadline: null,
42
+ responsibleUid: null,
43
+ assignedByUid: null,
44
+ isCollapsed: false,
45
+ isDeleted: false,
46
+ duration: null,
47
+ checked: false,
48
+ updatedAt: new Date().toISOString(),
49
+ due: null,
50
+ dayOrder: 0,
51
+ userId: 'creator-123',
52
+ completedAt: null,
53
+ childOrder: 1,
54
+ };
55
+ const mockProject = createMockProject({
56
+ id: 'project-123',
57
+ name: 'Test Project',
58
+ color: 'blue',
59
+ isShared: true,
60
+ canAssignTasks: true,
61
+ url: 'https://todoist.com/showProject?id=project-123',
62
+ });
63
+ beforeEach(() => {
64
+ jest.clearAllMocks();
65
+ mockTodoistApi = {
66
+ addTask: jest.fn(),
67
+ updateTask: jest.fn(),
68
+ getTask: jest.fn(),
69
+ getProjects: jest.fn(),
70
+ getProject: jest.fn(),
71
+ };
72
+ // Mock assignment validator responses
73
+ const mockAssignmentValidator = require('../../utils/assignment-validator.js').assignmentValidator;
74
+ mockAssignmentValidator.validateTaskCreationAssignment.mockResolvedValue({
75
+ isValid: true,
76
+ resolvedUser: mockValidUser,
77
+ });
78
+ mockAssignmentValidator.validateTaskUpdateAssignment.mockResolvedValue({
79
+ isValid: true,
80
+ resolvedUser: mockValidUser,
81
+ });
82
+ mockAssignmentValidator.validateBulkAssignment.mockResolvedValue([
83
+ { isValid: true, resolvedUser: mockValidUser },
84
+ { isValid: true, resolvedUser: mockValidUser },
85
+ { isValid: true, resolvedUser: mockValidUser },
86
+ ]);
87
+ // Mock user resolver
88
+ const mockUserResolver = require('../../utils/user-resolver.js').userResolver;
89
+ mockUserResolver.resolveUser.mockResolvedValue(mockValidUser);
90
+ mockUserResolver.getProjectCollaborators.mockResolvedValue([
91
+ { id: 'user-123', name: 'John Doe', email: 'john@example.com' },
92
+ { id: 'user-456', name: 'Jane Smith', email: 'jane@example.com' },
93
+ ]);
94
+ // Mock API responses
95
+ mockTodoistApi.getProjects.mockResolvedValue({
96
+ results: [mockProject],
97
+ nextCursor: null,
98
+ });
99
+ mockTodoistApi.getProject.mockResolvedValue(mockProject);
100
+ mockTodoistApi.addTask.mockResolvedValue({ ...mockTask, responsibleUid: 'user-123' });
101
+ mockTodoistApi.updateTask.mockResolvedValue({ ...mockTask, responsibleUid: 'user-123' });
102
+ mockTodoistApi.getTask.mockResolvedValue(mockTask);
103
+ });
104
+ describe('Task Creation with Assignment', () => {
105
+ it('should assign task during creation', async () => {
106
+ const result = await addTasks.execute({
107
+ tasks: [
108
+ {
109
+ content: 'New assigned task',
110
+ projectId: 'project-123',
111
+ responsibleUser: 'john@example.com',
112
+ },
113
+ ],
114
+ }, mockTodoistApi);
115
+ expect(mockTodoistApi.addTask).toHaveBeenCalledWith(expect.objectContaining({
116
+ content: 'New assigned task',
117
+ projectId: 'project-123',
118
+ assigneeId: 'user-123', // Should be resolved user ID
119
+ }));
120
+ expect(extractTextContent(result)).toContain('Added 1 task');
121
+ });
122
+ it('should validate assignment before creating task', async () => {
123
+ const mockAssignmentValidator = require('../../utils/assignment-validator.js').assignmentValidator;
124
+ mockAssignmentValidator.validateTaskCreationAssignment.mockResolvedValueOnce({
125
+ isValid: false,
126
+ error: {
127
+ message: 'User not found in project collaborators',
128
+ suggestions: ['Use find-project-collaborators to see valid assignees'],
129
+ },
130
+ });
131
+ await expect(addTasks.execute({
132
+ tasks: [
133
+ {
134
+ content: 'Invalid assignment task',
135
+ projectId: 'project-123',
136
+ responsibleUser: 'nonexistent@example.com',
137
+ },
138
+ ],
139
+ }, mockTodoistApi)).rejects.toThrow('Task "Invalid assignment task": User not found in project collaborators. Use find-project-collaborators to see valid assignees');
140
+ expect(mockTodoistApi.addTask).not.toHaveBeenCalled();
141
+ });
142
+ it('should handle assignment for subtasks', async () => {
143
+ mockTodoistApi.getTask.mockResolvedValueOnce({
144
+ ...mockTask,
145
+ id: 'parent-123',
146
+ projectId: 'project-123',
147
+ });
148
+ await addTasks.execute({
149
+ tasks: [
150
+ {
151
+ content: 'Subtask with assignment',
152
+ parentId: 'parent-123',
153
+ responsibleUser: 'john@example.com',
154
+ },
155
+ ],
156
+ }, mockTodoistApi);
157
+ expect(mockTodoistApi.addTask).toHaveBeenCalledWith(expect.objectContaining({
158
+ content: 'Subtask with assignment',
159
+ parentId: 'parent-123',
160
+ assigneeId: 'user-123',
161
+ }));
162
+ });
163
+ });
164
+ describe('Task Update with Assignment', () => {
165
+ it('should update task assignment', async () => {
166
+ const result = await updateTasks.execute({
167
+ tasks: [
168
+ {
169
+ id: 'task-123',
170
+ responsibleUser: 'jane@example.com',
171
+ },
172
+ ],
173
+ }, mockTodoistApi);
174
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('task-123', expect.objectContaining({
175
+ assigneeId: 'user-123',
176
+ }));
177
+ expect(extractTextContent(result)).toContain('Updated 1 task');
178
+ });
179
+ it('should unassign task when responsibleUser is null', async () => {
180
+ await updateTasks.execute({
181
+ tasks: [
182
+ {
183
+ id: 'task-123',
184
+ responsibleUser: null,
185
+ },
186
+ ],
187
+ }, mockTodoistApi);
188
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('task-123', expect.objectContaining({
189
+ assigneeId: null,
190
+ }));
191
+ });
192
+ it('should validate assignment changes', async () => {
193
+ const mockAssignmentValidator = require('../../utils/assignment-validator.js').assignmentValidator;
194
+ mockAssignmentValidator.validateTaskUpdateAssignment.mockResolvedValueOnce({
195
+ isValid: false,
196
+ error: {
197
+ message: 'User cannot be assigned to this project',
198
+ },
199
+ });
200
+ await expect(updateTasks.execute({
201
+ tasks: [
202
+ {
203
+ id: 'task-123',
204
+ responsibleUser: 'invalid@example.com',
205
+ },
206
+ ],
207
+ }, mockTodoistApi)).rejects.toThrow('Task task-123: User cannot be assigned to this project');
208
+ expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
209
+ });
210
+ });
211
+ describe('Bulk Assignment Operations', () => {
212
+ beforeEach(() => {
213
+ mockTodoistApi.getTask
214
+ .mockResolvedValueOnce({ ...mockTask, id: 'task-1' })
215
+ .mockResolvedValueOnce({ ...mockTask, id: 'task-2' })
216
+ .mockResolvedValueOnce({ ...mockTask, id: 'task-3' });
217
+ });
218
+ it('should perform bulk assignment', async () => {
219
+ const result = await manageAssignments.execute({
220
+ operation: 'assign',
221
+ taskIds: ['task-1', 'task-2', 'task-3'],
222
+ responsibleUser: 'john@example.com',
223
+ dryRun: false,
224
+ }, mockTodoistApi);
225
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledTimes(3);
226
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('task-1', {
227
+ assigneeId: 'user-123',
228
+ });
229
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('task-2', {
230
+ assigneeId: 'user-123',
231
+ });
232
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('task-3', {
233
+ assigneeId: 'user-123',
234
+ });
235
+ expect(extractTextContent(result)).toContain('3 tasks were successfully assigned');
236
+ });
237
+ it('should perform bulk unassignment', async () => {
238
+ const result = await manageAssignments.execute({
239
+ operation: 'unassign',
240
+ taskIds: ['task-1', 'task-2'],
241
+ dryRun: false,
242
+ }, mockTodoistApi);
243
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledTimes(2);
244
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('task-1', {
245
+ assigneeId: null,
246
+ });
247
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('task-2', {
248
+ assigneeId: null,
249
+ });
250
+ expect(extractTextContent(result)).toContain('2 tasks were successfully unassigned');
251
+ });
252
+ it('should handle dry-run mode', async () => {
253
+ // Mock validation for 2 tasks
254
+ const mockAssignmentValidator = require('../../utils/assignment-validator.js').assignmentValidator;
255
+ mockAssignmentValidator.validateBulkAssignment.mockResolvedValueOnce([
256
+ { isValid: true, resolvedUser: mockValidUser },
257
+ { isValid: true, resolvedUser: mockValidUser },
258
+ ]);
259
+ const result = await manageAssignments.execute({
260
+ operation: 'assign',
261
+ taskIds: ['task-1', 'task-2'],
262
+ responsibleUser: 'john@example.com',
263
+ dryRun: true,
264
+ }, mockTodoistApi);
265
+ expect(mockTodoistApi.updateTask).not.toHaveBeenCalled();
266
+ expect(extractTextContent(result)).toContain('Dry Run: Bulk assign operation');
267
+ expect(extractTextContent(result)).toContain('2 tasks would be successfully assigned');
268
+ });
269
+ it('should handle mixed success and failure results', async () => {
270
+ // Mock validation for 3 tasks - 2 valid, 1 invalid
271
+ const mockAssignmentValidator = require('../../utils/assignment-validator.js').assignmentValidator;
272
+ mockAssignmentValidator.validateBulkAssignment.mockResolvedValueOnce([
273
+ { isValid: true, resolvedUser: mockValidUser },
274
+ { isValid: false, error: { message: 'API Error' } },
275
+ { isValid: true, resolvedUser: mockValidUser },
276
+ ]);
277
+ mockTodoistApi.updateTask
278
+ .mockResolvedValueOnce({ ...mockTask, id: 'task-1' })
279
+ .mockResolvedValueOnce({ ...mockTask, id: 'task-3' });
280
+ const result = await manageAssignments.execute({
281
+ operation: 'assign',
282
+ taskIds: ['task-1', 'task-2', 'task-3'],
283
+ responsibleUser: 'john@example.com',
284
+ dryRun: false,
285
+ }, mockTodoistApi);
286
+ expect(extractTextContent(result)).toContain('2 tasks were successfully assigned');
287
+ expect(extractTextContent(result)).toContain('1 task failed');
288
+ expect(extractTextContent(result)).toContain('API Error');
289
+ });
290
+ });
291
+ describe('Project Collaborators Discovery', () => {
292
+ it('should find project collaborators', async () => {
293
+ const result = await findProjectCollaborators.execute({
294
+ projectId: 'project-123',
295
+ }, mockTodoistApi);
296
+ expect(extractTextContent(result)).toContain('Project collaborators');
297
+ expect(extractTextContent(result)).toContain('John Doe (john@example.com)');
298
+ expect(extractTextContent(result)).toContain('Jane Smith (jane@example.com)');
299
+ expect(extractStructuredContent(result).collaborators).toHaveLength(2);
300
+ });
301
+ it('should filter collaborators by search term', async () => {
302
+ const result = await findProjectCollaborators.execute({
303
+ projectId: 'project-123',
304
+ searchTerm: 'John',
305
+ }, mockTodoistApi);
306
+ expect(extractTextContent(result)).toContain('matching "John"');
307
+ });
308
+ it('should handle non-shared projects', async () => {
309
+ mockTodoistApi.getProject.mockResolvedValueOnce({ ...mockProject, isShared: false });
310
+ const result = await findProjectCollaborators.execute({
311
+ projectId: 'project-123',
312
+ }, mockTodoistApi);
313
+ expect(extractTextContent(result)).toContain('is not shared and has no collaborators');
314
+ expect(extractStructuredContent(result).collaborators).toHaveLength(0);
315
+ });
316
+ it('should handle project not found', async () => {
317
+ mockTodoistApi.getProject.mockRejectedValueOnce(new Error('Project not found'));
318
+ await expect(findProjectCollaborators.execute({
319
+ projectId: 'nonexistent-project',
320
+ }, mockTodoistApi)).rejects.toThrow('Failed to access project "nonexistent-project"');
321
+ });
322
+ });
323
+ describe('Error Handling and Edge Cases', () => {
324
+ it('should handle assignment validation errors gracefully', async () => {
325
+ const mockAssignmentValidator = require('../../utils/assignment-validator.js').assignmentValidator;
326
+ mockAssignmentValidator.validateTaskCreationAssignment.mockResolvedValueOnce({
327
+ isValid: false,
328
+ error: {
329
+ message: 'Project not shared',
330
+ suggestions: ['Share the project to enable assignments'],
331
+ },
332
+ });
333
+ await expect(addTasks.execute({
334
+ tasks: [
335
+ {
336
+ content: 'Task in unshared project',
337
+ projectId: 'project-123',
338
+ responsibleUser: 'john@example.com',
339
+ },
340
+ ],
341
+ }, mockTodoistApi)).rejects.toThrow('Task "Task in unshared project": Project not shared. Share the project to enable assignments');
342
+ });
343
+ it('should handle inbox assignment restriction', async () => {
344
+ await expect(addTasks.execute({
345
+ tasks: [
346
+ {
347
+ content: 'Inbox task with assignment',
348
+ responsibleUser: 'john@example.com',
349
+ },
350
+ ],
351
+ }, mockTodoistApi)).rejects.toThrow('Task "Inbox task with assignment": Cannot assign tasks without specifying project context. Please specify a projectId, sectionId, or parentId.');
352
+ });
353
+ it('should handle parent task not found', async () => {
354
+ mockTodoistApi.getTask.mockRejectedValueOnce(new Error('Task not found'));
355
+ await expect(addTasks.execute({
356
+ tasks: [
357
+ {
358
+ content: 'Subtask with bad parent',
359
+ parentId: 'nonexistent-parent',
360
+ responsibleUser: 'john@example.com',
361
+ },
362
+ ],
363
+ }, mockTodoistApi)).rejects.toThrow('Task "Subtask with bad parent": Parent task "nonexistent-parent" not found');
364
+ });
365
+ it('should require responsibleUser for assign operations', async () => {
366
+ await expect(manageAssignments.execute({
367
+ operation: 'assign',
368
+ taskIds: ['task-1'],
369
+ dryRun: false,
370
+ }, mockTodoistApi)).rejects.toThrow('assign operation requires responsibleUser parameter');
371
+ });
372
+ it('should require responsibleUser for reassign operations', async () => {
373
+ await expect(manageAssignments.execute({
374
+ operation: 'reassign',
375
+ taskIds: ['task-1'],
376
+ dryRun: false,
377
+ }, mockTodoistApi)).rejects.toThrow('reassign operation requires responsibleUser parameter');
378
+ });
379
+ });
380
+ describe('End-to-End Assignment Workflows', () => {
381
+ it('should support complete assignment lifecycle', async () => {
382
+ // 1. Create assigned task
383
+ const createResult = await addTasks.execute({
384
+ tasks: [
385
+ {
386
+ content: 'Task for lifecycle test',
387
+ projectId: 'project-123',
388
+ responsibleUser: 'john@example.com',
389
+ },
390
+ ],
391
+ }, mockTodoistApi);
392
+ expect(extractTextContent(createResult)).toContain('Added 1 task');
393
+ // 2. Update assignment
394
+ const updateResult = await updateTasks.execute({
395
+ tasks: [
396
+ {
397
+ id: 'task-123',
398
+ responsibleUser: 'jane@example.com',
399
+ },
400
+ ],
401
+ }, mockTodoistApi);
402
+ expect(extractTextContent(updateResult)).toContain('Updated 1 task');
403
+ // 3. Unassign task
404
+ const unassignResult = await updateTasks.execute({
405
+ tasks: [
406
+ {
407
+ id: 'task-123',
408
+ responsibleUser: null,
409
+ },
410
+ ],
411
+ }, mockTodoistApi);
412
+ expect(extractTextContent(unassignResult)).toContain('Updated 1 task');
413
+ });
414
+ });
415
+ });
@@ -1,5 +1,5 @@
1
1
  import { jest } from '@jest/globals';
2
- import { TEST_ERRORS, TEST_IDS, createMockApiResponse, createMockProject, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
2
+ import { createMockApiResponse, createMockProject, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { findProjects } from '../find-projects.js';
5
5
  // Mock the Todoist API
@@ -1,5 +1,5 @@
1
1
  import { jest } from '@jest/globals';
2
- import { TEST_ERRORS, TEST_IDS, createMockSection, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
2
+ import { createMockSection, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { findSections } from '../find-sections.js';
5
5
  // Mock the Todoist API
@@ -1,6 +1,6 @@
1
1
  import { jest } from '@jest/globals';
2
2
  import { getTasksByFilter } from '../../tool-helpers.js';
3
- import { TEST_ERRORS, TEST_IDS, createMappedTask, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
3
+ import { createMappedTask, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
4
4
  import { ToolNames } from '../../utils/tool-names.js';
5
5
  import { findTasksByDate } from '../find-tasks-by-date.js';
6
6
  // Mock the tool helpers
@@ -1,6 +1,6 @@
1
1
  import { jest } from '@jest/globals';
2
2
  import { getTasksByFilter } from '../../tool-helpers.js';
3
- import { TEST_ERRORS, TEST_IDS, TODAY, createMappedTask, createMockApiResponse, createMockTask, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
3
+ import { createMappedTask, createMockApiResponse, createMockTask, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, TODAY, } from '../../utils/test-helpers.js';
4
4
  import { ToolNames } from '../../utils/tool-names.js';
5
5
  import { findTasks } from '../find-tasks.js';
6
6
  jest.mock('../../tool-helpers', () => {
@@ -147,7 +147,7 @@ describe(`${FIND_TASKS} tool`, () => {
147
147
  });
148
148
  describe('validation', () => {
149
149
  it('should require at least one filter parameter', async () => {
150
- await expect(findTasks.execute({ limit: 10 }, mockTodoistApi)).rejects.toThrow('At least one filter must be provided: searchText, projectId, sectionId, parentId, or labels');
150
+ await expect(findTasks.execute({ limit: 10 }, mockTodoistApi)).rejects.toThrow('At least one filter must be provided: searchText, projectId, sectionId, parentId, responsibleUser, or labels');
151
151
  });
152
152
  });
153
153
  describe('container filtering', () => {
@@ -550,7 +550,7 @@ describe(`${FIND_TASKS} tool`, () => {
550
550
  describe('error handling', () => {
551
551
  it.each([
552
552
  {
553
- error: 'At least one filter must be provided: searchText, projectId, sectionId, parentId, or labels',
553
+ error: 'At least one filter must be provided: searchText, projectId, sectionId, parentId, responsibleUser, or labels',
554
554
  params: { limit: 10 },
555
555
  expectValidation: true,
556
556
  },
@@ -1,5 +1,5 @@
1
1
  import { jest } from '@jest/globals';
2
- import { TEST_ERRORS, TEST_IDS, createMockProject, createMockSection, createMockTask, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
2
+ import { createMockProject, createMockSection, createMockTask, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { getOverview } from '../get-overview.js';
5
5
  // Mock the Todoist API
@@ -1,5 +1,5 @@
1
1
  import { jest } from '@jest/globals';
2
- import { TEST_IDS, createMockTask, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
2
+ import { createMockTask, extractStructuredContent, extractTextContent, TEST_IDS, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { updateTasks } from '../update-tasks.js';
5
5
  // Mock the Todoist API
@@ -109,13 +109,13 @@ describe(`${UPDATE_TASKS} tool`, () => {
109
109
  tasks: [
110
110
  {
111
111
  id: '8485093749',
112
- priority: 3,
112
+ priority: 'p3',
113
113
  dueString: 'Aug 20',
114
114
  },
115
115
  ],
116
116
  }, mockTodoistApi);
117
117
  expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093749', {
118
- priority: 3,
118
+ priority: 2,
119
119
  dueString: 'Aug 20',
120
120
  });
121
121
  // Verify result structure
@@ -208,7 +208,7 @@ describe(`${UPDATE_TASKS} tool`, () => {
208
208
  id: '8485093752',
209
209
  content: 'Completely updated task',
210
210
  description: 'New description with details',
211
- priority: 4,
211
+ priority: 'p4',
212
212
  dueString: 'every Friday',
213
213
  projectId: 'different-project-id',
214
214
  },
@@ -222,7 +222,7 @@ describe(`${UPDATE_TASKS} tool`, () => {
222
222
  expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093752', {
223
223
  content: 'Completely updated task',
224
224
  description: 'New description with details',
225
- priority: 4,
225
+ priority: 1,
226
226
  dueString: 'every Friday',
227
227
  });
228
228
  // Verify result structure
@@ -339,6 +339,82 @@ describe(`${UPDATE_TASKS} tool`, () => {
339
339
  expect(structuredContent.tasks).toHaveLength(1);
340
340
  });
341
341
  });
342
+ describe('updating labels', () => {
343
+ it('should update task labels', async () => {
344
+ const mockApiResponse = createMockTask({
345
+ id: '8485093750',
346
+ content: 'Task with updated labels',
347
+ labels: ['work', 'important'],
348
+ url: 'https://todoist.com/showTask?id=8485093750',
349
+ addedAt: '2025-08-13T22:09:56.123456Z',
350
+ });
351
+ mockTodoistApi.updateTask.mockResolvedValue(mockApiResponse);
352
+ const result = await updateTasks.execute({
353
+ tasks: [
354
+ {
355
+ id: '8485093750',
356
+ labels: ['work', 'important'],
357
+ },
358
+ ],
359
+ }, mockTodoistApi);
360
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093750', {
361
+ labels: ['work', 'important'],
362
+ });
363
+ // Verify structured content includes updated labels
364
+ const structuredContent = extractStructuredContent(result);
365
+ expect(structuredContent.tasks).toHaveLength(1);
366
+ expect(structuredContent.tasks[0]).toEqual(expect.objectContaining({
367
+ labels: ['work', 'important'],
368
+ }));
369
+ });
370
+ it('should clear task labels with empty array', async () => {
371
+ const mockApiResponse = createMockTask({
372
+ id: '8485093751',
373
+ content: 'Task with cleared labels',
374
+ labels: [],
375
+ url: 'https://todoist.com/showTask?id=8485093751',
376
+ addedAt: '2025-08-13T22:09:56.123456Z',
377
+ });
378
+ mockTodoistApi.updateTask.mockResolvedValue(mockApiResponse);
379
+ await updateTasks.execute({
380
+ tasks: [
381
+ {
382
+ id: '8485093751',
383
+ labels: [],
384
+ },
385
+ ],
386
+ }, mockTodoistApi);
387
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093751', {
388
+ labels: [],
389
+ });
390
+ });
391
+ it('should update task with labels along with other fields', async () => {
392
+ const mockApiResponse = createMockTask({
393
+ id: '8485093752',
394
+ content: 'Updated content',
395
+ labels: ['personal', 'todo'],
396
+ priority: 3,
397
+ url: 'https://todoist.com/showTask?id=8485093752',
398
+ addedAt: '2025-08-13T22:09:56.123456Z',
399
+ });
400
+ mockTodoistApi.updateTask.mockResolvedValue(mockApiResponse);
401
+ await updateTasks.execute({
402
+ tasks: [
403
+ {
404
+ id: '8485093752',
405
+ content: 'Updated content',
406
+ labels: ['personal', 'todo'],
407
+ priority: 'p2',
408
+ },
409
+ ],
410
+ }, mockTodoistApi);
411
+ expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093752', {
412
+ content: 'Updated content',
413
+ labels: ['personal', 'todo'],
414
+ priority: 3,
415
+ });
416
+ });
417
+ });
342
418
  describe('error handling', () => {
343
419
  it('should throw error for invalid duration format', async () => {
344
420
  await expect(updateTasks.execute({
@@ -391,7 +467,7 @@ describe(`${UPDATE_TASKS} tool`, () => {
391
467
  },
392
468
  {
393
469
  error: 'API Error: Invalid priority value',
394
- params: { id: '8485093748', priority: 5 },
470
+ params: { id: '8485093748', content: 'Test task' },
395
471
  },
396
472
  ])('should propagate $error', async ({ error, params }) => {
397
473
  mockTodoistApi.updateTask.mockRejectedValue(new Error(error));
@@ -1,5 +1,5 @@
1
1
  import { jest } from '@jest/globals';
2
- import { TEST_ERRORS, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
2
+ import { extractStructuredContent, extractTextContent, TEST_ERRORS, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { userInfo } from '../user-info.js';
5
5
  // Mock the Todoist API
@@ -9,19 +9,19 @@ declare const addComments: {
9
9
  content: z.ZodString;
10
10
  }, "strip", z.ZodTypeAny, {
11
11
  content: string;
12
- projectId?: string | undefined;
13
12
  taskId?: string | undefined;
13
+ projectId?: string | undefined;
14
14
  }, {
15
15
  content: string;
16
- projectId?: string | undefined;
17
16
  taskId?: string | undefined;
17
+ projectId?: string | undefined;
18
18
  }>, "many">;
19
19
  };
20
20
  execute(args: {
21
21
  comments: {
22
22
  content: string;
23
- projectId?: string | undefined;
24
23
  taskId?: string | undefined;
24
+ projectId?: string | undefined;
25
25
  }[];
26
26
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
27
27
  content: {
@@ -1 +1 @@
1
- {"version":3,"file":"add-comments.d.ts","sourceRoot":"","sources":["../../src/tools/add-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAkBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAwFggV,CAAC;4BAA6C,CAAC;4BAA6C,CAAC;2BAA4C,CAAC;gCAAiD,CAAC;+BAAgD,CAAC;yBAA2D,CAAC;8BAA+C,CAAC;+BAAgD,CAAC;uBAAwC,CAAC;yBAA0C,CAAC;;;;;;;;;;;;;;;;;;;;;;;CA9Cr8V,CAAA;AA6C1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"add-comments.d.ts","sourceRoot":"","sources":["../../src/tools/add-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAkBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAoFygV,CAAC;4BAA6C,CAAC;4BAA6C,CAAC;2BAA4C,CAAC;gCAAiD,CAAC;+BAAgD,CAAC;yBAA2D,CAAC;8BAA+C,CAAC;+BAAgD,CAAC;uBAAwC,CAAC;yBAA0C,CAAC;;;;;;;;;;;;;;;;;;;;;;;CA1C98V,CAAA;AAyC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -42,7 +42,7 @@ const addComments = {
42
42
  });
43
43
  },
44
44
  };
45
- function generateTextContent({ comments, }) {
45
+ function generateTextContent({ comments }) {
46
46
  // Group comments by entity type and count
47
47
  const taskComments = comments.filter((c) => c.taskId).length;
48
48
  const projectComments = comments.filter((c) => c.projectId).length;
@@ -1 +1 @@
1
- {"version":3,"file":"add-projects.d.ts","sourceRoot":"","sources":["../../src/tools/add-projects.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAwBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgByB,CAAA;AAmC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"add-projects.d.ts","sourceRoot":"","sources":["../../src/tools/add-projects.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAwBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgByB,CAAA;AA+B1C,OAAO,EAAE,WAAW,EAAE,CAAA"}