@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.
- package/dist/index.d.ts +160 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -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 +78 -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-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 +1 -1
- package/dist/tools/__tests__/find-tasks.test.js +3 -3
- package/dist/tools/__tests__/get-overview.test.js +1 -1
- package/dist/tools/__tests__/update-tasks.test.js +6 -6
- package/dist/tools/__tests__/user-info.test.js +1 -1
- package/dist/tools/add-comments.d.ts +3 -3
- 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.map +1 -1
- package/dist/tools/add-projects.js +1 -1
- 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 +13 -7
- 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 +2 -2
- package/dist/tools/find-completed-tasks.d.ts +4 -2
- package/dist/tools/find-completed-tasks.d.ts.map +1 -1
- package/dist/tools/find-completed-tasks.js +2 -2
- 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 +2 -0
- package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
- package/dist/tools/find-tasks-by-date.js +2 -2
- package/dist/tools/find-tasks.d.ts +7 -2
- package/dist/tools/find-tasks.d.ts.map +1 -1
- package/dist/tools/find-tasks.js +128 -33
- 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.map +1 -1
- package/dist/tools/update-comments.js +1 -1
- 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 +13 -7
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/tools/update-tasks.js +32 -9
- 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/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 +2 -0
- package/dist/utils/tool-names.d.ts +2 -0
- package/dist/utils/tool-names.d.ts.map +1 -1
- package/dist/utils/tool-names.js +3 -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 +5 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
{
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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 @@
|
|
|
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 {
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
2
|
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
-
import {
|
|
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 {
|
|
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 {
|
|
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
|