@doist/todoist-ai 4.1.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.
- package/dist/index.d.ts +405 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -16
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-helpers.js +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +80 -17
- package/dist/tool-helpers.d.ts +4 -0
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +2 -0
- package/dist/tools/__tests__/add-projects.test.js +1 -1
- package/dist/tools/__tests__/add-sections.test.js +1 -1
- package/dist/tools/__tests__/add-tasks.test.js +52 -13
- package/dist/tools/__tests__/assignment-integration.test.d.ts +2 -0
- package/dist/tools/__tests__/assignment-integration.test.d.ts.map +1 -0
- package/dist/tools/__tests__/assignment-integration.test.js +415 -0
- package/dist/tools/__tests__/find-completed-tasks.test.js +136 -2
- package/dist/tools/__tests__/find-projects.test.js +1 -1
- package/dist/tools/__tests__/find-sections.test.js +1 -1
- package/dist/tools/__tests__/find-tasks-by-date.test.js +122 -3
- package/dist/tools/__tests__/find-tasks.test.js +258 -11
- package/dist/tools/__tests__/get-overview.test.js +1 -1
- package/dist/tools/__tests__/update-sections.test.js +1 -0
- package/dist/tools/__tests__/update-tasks.test.js +6 -6
- package/dist/tools/__tests__/user-info.test.d.ts +2 -0
- package/dist/tools/__tests__/user-info.test.d.ts.map +1 -0
- package/dist/tools/__tests__/user-info.test.js +139 -0
- package/dist/tools/add-comments.d.ts +28 -5
- package/dist/tools/add-comments.d.ts.map +1 -1
- package/dist/tools/add-comments.js +1 -1
- package/dist/tools/add-projects.d.ts +46 -2
- package/dist/tools/add-projects.d.ts.map +1 -1
- package/dist/tools/add-projects.js +1 -1
- package/dist/tools/add-sections.d.ts +14 -2
- package/dist/tools/add-sections.d.ts.map +1 -1
- package/dist/tools/add-sections.js +1 -1
- package/dist/tools/add-tasks.d.ts +16 -10
- package/dist/tools/add-tasks.d.ts.map +1 -1
- package/dist/tools/add-tasks.js +49 -3
- package/dist/tools/find-comments.d.ts +27 -4
- package/dist/tools/find-comments.d.ts.map +1 -1
- package/dist/tools/find-completed-tasks.d.ts +12 -4
- package/dist/tools/find-completed-tasks.d.ts.map +1 -1
- package/dist/tools/find-completed-tasks.js +20 -4
- package/dist/tools/find-project-collaborators.d.ts +64 -0
- package/dist/tools/find-project-collaborators.d.ts.map +1 -0
- package/dist/tools/find-project-collaborators.js +151 -0
- package/dist/tools/find-tasks-by-date.d.ts +8 -0
- package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
- package/dist/tools/find-tasks-by-date.js +19 -2
- package/dist/tools/find-tasks.d.ts +13 -2
- package/dist/tools/find-tasks.d.ts.map +1 -1
- package/dist/tools/find-tasks.js +172 -23
- package/dist/tools/get-overview.d.ts +2 -2
- package/dist/tools/get-overview.d.ts.map +1 -1
- package/dist/tools/get-overview.js +1 -1
- package/dist/tools/manage-assignments.d.ts +52 -0
- package/dist/tools/manage-assignments.d.ts.map +1 -0
- package/dist/tools/manage-assignments.js +337 -0
- package/dist/tools/update-comments.d.ts +25 -2
- package/dist/tools/update-comments.d.ts.map +1 -1
- package/dist/tools/update-comments.js +1 -1
- package/dist/tools/update-projects.d.ts +46 -2
- package/dist/tools/update-projects.d.ts.map +1 -1
- package/dist/tools/update-sections.d.ts +14 -2
- package/dist/tools/update-sections.d.ts.map +1 -1
- package/dist/tools/update-sections.js +1 -1
- package/dist/tools/update-tasks.d.ts +16 -10
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/tools/update-tasks.js +32 -9
- package/dist/tools/user-info.d.ts +44 -0
- package/dist/tools/user-info.d.ts.map +1 -0
- package/dist/tools/user-info.js +142 -0
- package/dist/utils/assignment-validator.d.ts +69 -0
- package/dist/utils/assignment-validator.d.ts.map +1 -0
- package/dist/utils/assignment-validator.js +253 -0
- package/dist/utils/duration-parser.d.ts +2 -2
- package/dist/utils/duration-parser.d.ts.map +1 -1
- package/dist/utils/labels.d.ts +10 -0
- package/dist/utils/labels.d.ts.map +1 -0
- package/dist/utils/labels.js +18 -0
- package/dist/utils/priorities.d.ts +8 -0
- package/dist/utils/priorities.d.ts.map +1 -0
- package/dist/utils/priorities.js +15 -0
- package/dist/utils/response-builders.d.ts +2 -2
- package/dist/utils/response-builders.d.ts.map +1 -1
- package/dist/utils/response-builders.js +8 -1
- package/dist/utils/test-helpers.d.ts +2 -0
- package/dist/utils/test-helpers.d.ts.map +1 -1
- package/dist/utils/test-helpers.js +3 -0
- package/dist/utils/tool-names.d.ts +3 -0
- package/dist/utils/tool-names.d.ts.map +1 -1
- package/dist/utils/tool-names.js +4 -0
- package/dist/utils/user-resolver.d.ts +39 -0
- package/dist/utils/user-resolver.d.ts.map +1 -0
- package/dist/utils/user-resolver.js +179 -0
- package/package.json +7 -7
|
@@ -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
|
+
});
|
|
@@ -37,7 +37,14 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
37
37
|
items: mockCompletedTasks,
|
|
38
38
|
nextCursor: null,
|
|
39
39
|
});
|
|
40
|
-
const result = await findCompletedTasks.execute({
|
|
40
|
+
const result = await findCompletedTasks.execute({
|
|
41
|
+
getBy: 'completion',
|
|
42
|
+
limit: 50,
|
|
43
|
+
since: '2025-08-10',
|
|
44
|
+
until: '2025-08-15',
|
|
45
|
+
labels: [],
|
|
46
|
+
labelsOperator: 'or',
|
|
47
|
+
}, mockTodoistApi);
|
|
41
48
|
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
42
49
|
since: '2025-08-10',
|
|
43
50
|
until: '2025-08-15',
|
|
@@ -57,6 +64,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
57
64
|
until: '2025-08-31',
|
|
58
65
|
projectId: 'specific-project-id',
|
|
59
66
|
cursor: 'current-cursor',
|
|
67
|
+
labels: [],
|
|
68
|
+
labelsOperator: 'or',
|
|
60
69
|
}, mockTodoistApi);
|
|
61
70
|
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
62
71
|
since: '2025-08-01',
|
|
@@ -98,6 +107,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
98
107
|
limit: 50,
|
|
99
108
|
since: '2025-08-10',
|
|
100
109
|
until: '2025-08-20',
|
|
110
|
+
labels: [],
|
|
111
|
+
labelsOperator: 'or',
|
|
101
112
|
}, mockTodoistApi);
|
|
102
113
|
expect(mockTodoistApi.getCompletedTasksByDueDate).toHaveBeenCalledWith({
|
|
103
114
|
since: '2025-08-10',
|
|
@@ -108,13 +119,134 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
108
119
|
expect(extractTextContent(result)).toMatchSnapshot();
|
|
109
120
|
});
|
|
110
121
|
});
|
|
122
|
+
describe('label filtering', () => {
|
|
123
|
+
it.each([
|
|
124
|
+
{
|
|
125
|
+
name: 'single label with OR operator',
|
|
126
|
+
params: {
|
|
127
|
+
getBy: 'completion',
|
|
128
|
+
since: '2025-08-01',
|
|
129
|
+
until: '2025-08-31',
|
|
130
|
+
limit: 50,
|
|
131
|
+
labels: ['work'],
|
|
132
|
+
},
|
|
133
|
+
expectedMethod: 'getCompletedTasksByCompletionDate',
|
|
134
|
+
expectedFilter: '(@work)',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'multiple labels with AND operator',
|
|
138
|
+
params: {
|
|
139
|
+
getBy: 'due',
|
|
140
|
+
since: '2025-08-01',
|
|
141
|
+
until: '2025-08-31',
|
|
142
|
+
limit: 50,
|
|
143
|
+
labels: ['work', 'urgent'],
|
|
144
|
+
labelsOperator: 'and',
|
|
145
|
+
},
|
|
146
|
+
expectedMethod: 'getCompletedTasksByDueDate',
|
|
147
|
+
expectedFilter: '(@work & @urgent)',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'multiple labels with OR operator',
|
|
151
|
+
params: {
|
|
152
|
+
getBy: 'completion',
|
|
153
|
+
since: '2025-08-10',
|
|
154
|
+
until: '2025-08-20',
|
|
155
|
+
limit: 25,
|
|
156
|
+
labels: ['personal', 'shopping'],
|
|
157
|
+
},
|
|
158
|
+
expectedMethod: 'getCompletedTasksByCompletionDate',
|
|
159
|
+
expectedFilter: '(@personal | @shopping)',
|
|
160
|
+
},
|
|
161
|
+
])('should filter completed tasks by labels: $name', async ({ params, expectedMethod, expectedFilter }) => {
|
|
162
|
+
const mockCompletedTasks = [
|
|
163
|
+
createMockTask({
|
|
164
|
+
id: '8485093748',
|
|
165
|
+
content: 'Completed task with label',
|
|
166
|
+
labels: params.labels,
|
|
167
|
+
completedAt: '2024-01-01T00:00:00Z',
|
|
168
|
+
}),
|
|
169
|
+
];
|
|
170
|
+
const mockResponse = { items: mockCompletedTasks, nextCursor: null };
|
|
171
|
+
const mockMethod = mockTodoistApi[expectedMethod];
|
|
172
|
+
mockMethod.mockResolvedValue(mockResponse);
|
|
173
|
+
const result = await findCompletedTasks.execute(params, mockTodoistApi);
|
|
174
|
+
expect(mockMethod).toHaveBeenCalledWith({
|
|
175
|
+
since: params.since,
|
|
176
|
+
until: params.until,
|
|
177
|
+
limit: params.limit,
|
|
178
|
+
filterQuery: expectedFilter,
|
|
179
|
+
filterLang: 'en',
|
|
180
|
+
});
|
|
181
|
+
const textContent = extractTextContent(result);
|
|
182
|
+
expect(textContent).toMatchSnapshot();
|
|
183
|
+
});
|
|
184
|
+
it('should handle empty labels array', async () => {
|
|
185
|
+
const params = {
|
|
186
|
+
getBy: 'completion',
|
|
187
|
+
since: '2025-08-01',
|
|
188
|
+
until: '2025-08-31',
|
|
189
|
+
limit: 50,
|
|
190
|
+
labels: [],
|
|
191
|
+
labelsOperator: 'or',
|
|
192
|
+
};
|
|
193
|
+
const mockResponse = { items: [], nextCursor: null };
|
|
194
|
+
mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue(mockResponse);
|
|
195
|
+
await findCompletedTasks.execute(params, mockTodoistApi);
|
|
196
|
+
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
197
|
+
since: params.since,
|
|
198
|
+
until: params.until,
|
|
199
|
+
limit: params.limit,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('should combine other filters with label filters', async () => {
|
|
203
|
+
const params = {
|
|
204
|
+
getBy: 'due',
|
|
205
|
+
since: '2025-08-01',
|
|
206
|
+
until: '2025-08-31',
|
|
207
|
+
limit: 25,
|
|
208
|
+
projectId: 'test-project-id',
|
|
209
|
+
sectionId: 'test-section-id',
|
|
210
|
+
labels: ['important'],
|
|
211
|
+
labelsOperator: 'or',
|
|
212
|
+
};
|
|
213
|
+
const mockTasks = [
|
|
214
|
+
createMockTask({
|
|
215
|
+
content: 'Important completed task',
|
|
216
|
+
labels: ['important'],
|
|
217
|
+
completedAt: '2024-01-01T00:00:00Z',
|
|
218
|
+
}),
|
|
219
|
+
];
|
|
220
|
+
const mockResponse = { items: mockTasks, nextCursor: null };
|
|
221
|
+
mockTodoistApi.getCompletedTasksByDueDate.mockResolvedValue(mockResponse);
|
|
222
|
+
const result = await findCompletedTasks.execute(params, mockTodoistApi);
|
|
223
|
+
expect(mockTodoistApi.getCompletedTasksByDueDate).toHaveBeenCalledWith({
|
|
224
|
+
since: params.since,
|
|
225
|
+
until: params.until,
|
|
226
|
+
limit: params.limit,
|
|
227
|
+
projectId: params.projectId,
|
|
228
|
+
sectionId: params.sectionId,
|
|
229
|
+
filterQuery: '(@important)',
|
|
230
|
+
filterLang: 'en',
|
|
231
|
+
});
|
|
232
|
+
const textContent = extractTextContent(result);
|
|
233
|
+
expect(textContent).toMatchSnapshot();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
111
236
|
describe('error handling', () => {
|
|
112
237
|
it('should propagate completion date API errors', async () => {
|
|
113
238
|
const apiError = new Error('API Error: Invalid date range');
|
|
114
239
|
mockTodoistApi.getCompletedTasksByCompletionDate.mockRejectedValue(apiError);
|
|
115
240
|
await expect(findCompletedTasks.execute(
|
|
116
241
|
// invalid date range
|
|
117
|
-
{
|
|
242
|
+
{
|
|
243
|
+
getBy: 'completion',
|
|
244
|
+
limit: 50,
|
|
245
|
+
since: '2025-08-31',
|
|
246
|
+
until: '2025-08-01',
|
|
247
|
+
labels: [],
|
|
248
|
+
labelsOperator: 'or',
|
|
249
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Invalid date range');
|
|
118
250
|
});
|
|
119
251
|
it('should propagate due date API errors', async () => {
|
|
120
252
|
const apiError = new Error('API Error: Project not found');
|
|
@@ -125,6 +257,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
125
257
|
since: '2025-08-01',
|
|
126
258
|
until: '2025-08-31',
|
|
127
259
|
projectId: 'non-existent-project',
|
|
260
|
+
labels: [],
|
|
261
|
+
labelsOperator: 'or',
|
|
128
262
|
}, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
|
|
129
263
|
});
|
|
130
264
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
|
-
import {
|
|
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 {
|
|
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
|