@doist/todoist-ai 4.4.0 → 4.5.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.
Files changed (80) hide show
  1. package/dist/index.d.ts +160 -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 +52 -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 +6 -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 +13 -7
  32. package/dist/tools/add-tasks.d.ts.map +1 -1
  33. package/dist/tools/add-tasks.js +49 -3
  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 +13 -7
  58. package/dist/tools/update-tasks.d.ts.map +1 -1
  59. package/dist/tools/update-tasks.js +32 -9
  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 +5 -5
@@ -1,5 +1,5 @@
1
1
  import { jest } from '@jest/globals';
2
- import { TEST_IDS, createMockSection, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
2
+ import { createMockSection, extractStructuredContent, extractTextContent, TEST_IDS, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { addSections } from '../add-sections.js';
5
5
  // Mock the Todoist API
@@ -1,5 +1,5 @@
1
1
  import { jest } from '@jest/globals';
2
- import { TODAY, createMockTask, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
2
+ import { createMockTask, extractStructuredContent, extractTextContent, TODAY, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { addTasks } from '../add-tasks.js';
5
5
  // Mock the Todoist API
@@ -42,11 +42,14 @@ describe(`${ADD_TASKS} tool`, () => {
42
42
  .mockResolvedValueOnce(mockApiResponse2);
43
43
  const result = await addTasks.execute({
44
44
  tasks: [
45
- { content: 'First task content', projectId: '6cfCcrrCFg2xP94Q' },
45
+ {
46
+ content: 'First task content',
47
+ projectId: '6cfCcrrCFg2xP94Q',
48
+ },
46
49
  {
47
50
  content: 'Second task content',
48
51
  description: 'Task description',
49
- priority: 2,
52
+ priority: 'p2',
50
53
  dueString: 'Aug 15',
51
54
  projectId: '6cfCcrrCFg2xP94Q',
52
55
  },
@@ -63,7 +66,7 @@ describe(`${ADD_TASKS} tool`, () => {
63
66
  expect(mockTodoistApi.addTask).toHaveBeenNthCalledWith(2, {
64
67
  content: 'Second task content',
65
68
  description: 'Task description',
66
- priority: 2,
69
+ priority: 3,
67
70
  dueString: 'Aug 15',
68
71
  projectId: '6cfCcrrCFg2xP94Q',
69
72
  sectionId: undefined,
@@ -99,7 +102,7 @@ describe(`${ADD_TASKS} tool`, () => {
99
102
  {
100
103
  content: 'Subtask content',
101
104
  description: 'Subtask description',
102
- priority: 3,
105
+ priority: 'p3',
103
106
  projectId: '6cfCcrrCFg2xP94Q',
104
107
  sectionId: 'section-123',
105
108
  parentId: 'parent-task-456',
@@ -109,7 +112,7 @@ describe(`${ADD_TASKS} tool`, () => {
109
112
  expect(mockTodoistApi.addTask).toHaveBeenCalledWith({
110
113
  content: 'Subtask content',
111
114
  description: 'Subtask description',
112
- priority: 3,
115
+ priority: 2,
113
116
  projectId: '6cfCcrrCFg2xP94Q',
114
117
  sectionId: 'section-123',
115
118
  parentId: 'parent-task-456',
@@ -203,7 +206,15 @@ describe(`${ADD_TASKS} tool`, () => {
203
206
  ];
204
207
  for (const testCase of testCases) {
205
208
  mockTodoistApi.addTask.mockClear();
206
- await addTasks.execute({ tasks: [{ content: 'Test task', duration: testCase.input }] }, mockTodoistApi);
209
+ await addTasks.execute({
210
+ tasks: [
211
+ {
212
+ content: 'Test task',
213
+ duration: testCase.input,
214
+ projectId: '6cfCcrrCFg2xP94Q',
215
+ },
216
+ ],
217
+ }, mockTodoistApi);
207
218
  expect(mockTodoistApi.addTask).toHaveBeenCalledWith(expect.objectContaining({
208
219
  duration: testCase.expectedMinutes,
209
220
  durationUnit: 'minute',
@@ -213,15 +224,31 @@ describe(`${ADD_TASKS} tool`, () => {
213
224
  });
214
225
  describe('error handling', () => {
215
226
  it('should throw error for invalid duration format', async () => {
216
- await expect(addTasks.execute({ tasks: [{ content: 'Task with invalid duration', duration: 'invalid' }] }, mockTodoistApi)).rejects.toThrow('Task "Task with invalid duration": Invalid duration format "invalid"');
227
+ await expect(addTasks.execute({
228
+ tasks: [
229
+ {
230
+ content: 'Task with invalid duration',
231
+ duration: 'invalid',
232
+ projectId: '6cfCcrrCFg2xP94Q',
233
+ },
234
+ ],
235
+ }, mockTodoistApi)).rejects.toThrow('Task "Task with invalid duration": Invalid duration format "invalid"');
217
236
  });
218
237
  it('should throw error for duration exceeding 24 hours', async () => {
219
- await expect(addTasks.execute({ tasks: [{ content: 'Task with too long duration', duration: '25h' }] }, mockTodoistApi)).rejects.toThrow('Task "Task with too long duration": Invalid duration format "25h": Duration cannot exceed 24 hours (1440 minutes)');
238
+ await expect(addTasks.execute({
239
+ tasks: [
240
+ {
241
+ content: 'Task with too long duration',
242
+ duration: '25h',
243
+ projectId: '6cfCcrrCFg2xP94Q',
244
+ },
245
+ ],
246
+ }, mockTodoistApi)).rejects.toThrow('Task "Task with too long duration": Invalid duration format "25h": Duration cannot exceed 24 hours (1440 minutes)');
220
247
  });
221
248
  it('should propagate API errors', async () => {
222
249
  const apiError = new Error('API Error: Task content is required');
223
250
  mockTodoistApi.addTask.mockRejectedValue(apiError);
224
- await expect(addTasks.execute({ tasks: [{ content: '' }] }, mockTodoistApi)).rejects.toThrow(apiError.message);
251
+ await expect(addTasks.execute({ tasks: [{ content: '', projectId: '6cfCcrrCFg2xP94Q' }] }, mockTodoistApi)).rejects.toThrow(apiError.message);
225
252
  });
226
253
  it('should handle partial failures when adding multiple tasks', async () => {
227
254
  const mockApiResponse = createMockTask({
@@ -236,8 +263,8 @@ describe(`${ADD_TASKS} tool`, () => {
236
263
  .mockRejectedValueOnce(apiError);
237
264
  await expect(addTasks.execute({
238
265
  tasks: [
239
- { content: 'First task content' },
240
- { content: 'Second task content' },
266
+ { content: 'First task content', projectId: '6cfCcrrCFg2xP94Q' },
267
+ { content: 'Second task content', projectId: '6cfCcrrCFg2xP94Q' },
241
268
  ],
242
269
  }, mockTodoistApi)).rejects.toThrow('API Error: Second task failed');
243
270
  // Verify first task was attempted
@@ -246,6 +273,8 @@ describe(`${ADD_TASKS} tool`, () => {
246
273
  });
247
274
  describe('next steps logic', () => {
248
275
  it('should suggest find-tasks-by-date for today when hasToday is true', async () => {
276
+ // Clear any leftover mocks from previous tests
277
+ mockTodoistApi.addTask.mockClear();
249
278
  const mockApiResponse = createMockTask({
250
279
  id: '8485093755',
251
280
  content: 'Task due today',
@@ -260,12 +289,22 @@ describe(`${ADD_TASKS} tool`, () => {
260
289
  },
261
290
  });
262
291
  mockTodoistApi.addTask.mockResolvedValue(mockApiResponse);
263
- const result = await addTasks.execute({ tasks: [{ content: 'Task due today', dueString: 'today' }] }, mockTodoistApi);
292
+ const result = await addTasks.execute({
293
+ tasks: [
294
+ {
295
+ content: 'Task due today',
296
+ dueString: 'today',
297
+ projectId: '6cfCcrrCFg2xP94Q',
298
+ },
299
+ ],
300
+ }, mockTodoistApi);
264
301
  const textContent = extractTextContent(result);
265
302
  expect(textContent).toMatchSnapshot();
266
303
  expect(textContent).toContain(`Use ${GET_OVERVIEW} to see your updated project organization`);
267
304
  });
268
305
  it('should suggest overview tool when no hasToday context', async () => {
306
+ // Clear any leftover mocks from previous tests
307
+ mockTodoistApi.addTask.mockClear();
269
308
  const mockApiResponse = createMockTask({
270
309
  id: '8485093756',
271
310
  content: 'Regular task',
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=assignment-integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignment-integration.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/assignment-integration.test.ts"],"names":[],"mappings":""}
@@ -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