@doist/todoist-ai 4.1.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/index.d.ts +246 -16
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -1
  4. package/dist/mcp-server.d.ts.map +1 -1
  5. package/dist/mcp-server.js +2 -0
  6. package/dist/tools/__tests__/find-completed-tasks.test.js +136 -2
  7. package/dist/tools/__tests__/find-tasks-by-date.test.js +121 -2
  8. package/dist/tools/__tests__/find-tasks.test.js +257 -10
  9. package/dist/tools/__tests__/update-sections.test.js +1 -0
  10. package/dist/tools/__tests__/user-info.test.d.ts +2 -0
  11. package/dist/tools/__tests__/user-info.test.d.ts.map +1 -0
  12. package/dist/tools/__tests__/user-info.test.js +139 -0
  13. package/dist/tools/add-comments.d.ts +25 -2
  14. package/dist/tools/add-comments.d.ts.map +1 -1
  15. package/dist/tools/add-projects.d.ts +46 -2
  16. package/dist/tools/add-projects.d.ts.map +1 -1
  17. package/dist/tools/add-sections.d.ts +14 -2
  18. package/dist/tools/add-sections.d.ts.map +1 -1
  19. package/dist/tools/add-tasks.d.ts +3 -3
  20. package/dist/tools/find-comments.d.ts +25 -2
  21. package/dist/tools/find-comments.d.ts.map +1 -1
  22. package/dist/tools/find-completed-tasks.d.ts +8 -2
  23. package/dist/tools/find-completed-tasks.d.ts.map +1 -1
  24. package/dist/tools/find-completed-tasks.js +19 -3
  25. package/dist/tools/find-tasks-by-date.d.ts +6 -0
  26. package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
  27. package/dist/tools/find-tasks-by-date.js +18 -1
  28. package/dist/tools/find-tasks.d.ts +6 -0
  29. package/dist/tools/find-tasks.d.ts.map +1 -1
  30. package/dist/tools/find-tasks.js +63 -9
  31. package/dist/tools/update-comments.d.ts +25 -2
  32. package/dist/tools/update-comments.d.ts.map +1 -1
  33. package/dist/tools/update-projects.d.ts +46 -2
  34. package/dist/tools/update-projects.d.ts.map +1 -1
  35. package/dist/tools/update-sections.d.ts +14 -2
  36. package/dist/tools/update-sections.d.ts.map +1 -1
  37. package/dist/tools/update-tasks.d.ts +3 -3
  38. package/dist/tools/user-info.d.ts +44 -0
  39. package/dist/tools/user-info.d.ts.map +1 -0
  40. package/dist/tools/user-info.js +142 -0
  41. package/dist/utils/labels.d.ts +10 -0
  42. package/dist/utils/labels.d.ts.map +1 -0
  43. package/dist/utils/labels.js +18 -0
  44. package/dist/utils/test-helpers.d.ts.map +1 -1
  45. package/dist/utils/test-helpers.js +1 -0
  46. package/dist/utils/tool-names.d.ts +1 -0
  47. package/dist/utils/tool-names.d.ts.map +1 -1
  48. package/dist/utils/tool-names.js +1 -0
  49. package/package.json +3 -3
@@ -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({ getBy: 'completion', limit: 50, since: '2025-08-10', until: '2025-08-15' }, mockTodoistApi);
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
- { getBy: 'completion', limit: 50, since: '2025-08-31', until: '2025-08-01' }, mockTodoistApi)).rejects.toThrow('API Error: Invalid date range');
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
  });
@@ -193,7 +193,11 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
193
193
  ];
194
194
  const mockResponse = { tasks: mockTasks, nextCursor: null };
195
195
  mockGetTasksByFilter.mockResolvedValue(mockResponse);
196
- const result = await findTasksByDate.execute({ startDate: '2025-08-15', limit: 10, daysCount: 1 }, mockTodoistApi);
196
+ const result = await findTasksByDate.execute({
197
+ startDate: '2025-08-15',
198
+ limit: 10,
199
+ daysCount: 1,
200
+ }, mockTodoistApi);
197
201
  const textContent = extractTextContent(result);
198
202
  expect(textContent).toMatchSnapshot();
199
203
  expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
@@ -240,13 +244,128 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
240
244
  it('should provide helpful suggestions for empty date range results', async () => {
241
245
  const mockResponse = { tasks: [], nextCursor: null };
242
246
  mockGetTasksByFilter.mockResolvedValue(mockResponse);
243
- const result = await findTasksByDate.execute({ startDate: '2025-08-20', limit: 10, daysCount: 1 }, mockTodoistApi);
247
+ const result = await findTasksByDate.execute({
248
+ startDate: '2025-08-20',
249
+ limit: 10,
250
+ daysCount: 1,
251
+ }, mockTodoistApi);
244
252
  const textContent = extractTextContent(result);
245
253
  expect(textContent).toMatchSnapshot();
246
254
  expect(textContent).toContain("Expand date range with larger 'daysCount'");
247
255
  expect(textContent).toContain("Check 'overdue' for past-due items");
248
256
  });
249
257
  });
258
+ describe('label filtering', () => {
259
+ it.each([
260
+ {
261
+ name: 'single label with OR operator',
262
+ params: {
263
+ startDate: 'today',
264
+ daysCount: 1,
265
+ limit: 50,
266
+ labels: ['work'],
267
+ },
268
+ expectedQueryPattern: '((@work))', // Will be combined with date query
269
+ },
270
+ {
271
+ name: 'multiple labels with AND operator',
272
+ params: {
273
+ startDate: 'overdue',
274
+ daysCount: 1,
275
+ limit: 50,
276
+ labels: ['work', 'urgent'],
277
+ labelsOperator: 'and',
278
+ },
279
+ expectedQueryPattern: 'overdue & ((@work & @urgent))',
280
+ },
281
+ {
282
+ name: 'multiple labels with OR operator',
283
+ params: {
284
+ startDate: '2025-08-20',
285
+ daysCount: 3,
286
+ limit: 50,
287
+ labels: ['personal', 'shopping'],
288
+ labelsOperator: 'or',
289
+ },
290
+ expectedQueryPattern: '((@personal | @shopping))',
291
+ },
292
+ ])('should filter tasks by labels: $name', async ({ params, expectedQueryPattern }) => {
293
+ const mockTasks = [
294
+ createMappedTask({
295
+ id: TEST_IDS.TASK_1,
296
+ content: 'Task with work label',
297
+ labels: ['work'],
298
+ dueDate: '2025-08-20',
299
+ }),
300
+ ];
301
+ const mockResponse = { tasks: mockTasks, nextCursor: null };
302
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
303
+ const result = await findTasksByDate.execute(params, mockTodoistApi);
304
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
305
+ client: mockTodoistApi,
306
+ query: expect.stringContaining('(@'),
307
+ cursor: undefined,
308
+ limit: 50,
309
+ });
310
+ // For overdue specifically, check the exact pattern
311
+ if (params.startDate === 'overdue') {
312
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
313
+ client: mockTodoistApi,
314
+ query: expectedQueryPattern,
315
+ cursor: undefined,
316
+ limit: 50,
317
+ });
318
+ }
319
+ const structuredContent = extractStructuredContent(result);
320
+ expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
321
+ labels: params.labels,
322
+ ...(params.labelsOperator ? { labelsOperator: params.labelsOperator } : {}),
323
+ }));
324
+ });
325
+ it('should handle empty labels array', async () => {
326
+ const params = {
327
+ startDate: 'today',
328
+ daysCount: 1,
329
+ limit: 50,
330
+ };
331
+ const mockResponse = { tasks: [], nextCursor: null };
332
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
333
+ await findTasksByDate.execute(params, mockTodoistApi);
334
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
335
+ client: mockTodoistApi,
336
+ query: expect.not.stringContaining('@'),
337
+ cursor: undefined,
338
+ limit: 50,
339
+ });
340
+ });
341
+ it('should combine date filters with label filters', async () => {
342
+ const params = {
343
+ startDate: '2025-08-15',
344
+ daysCount: 1,
345
+ limit: 25,
346
+ labels: ['important'],
347
+ };
348
+ const mockTasks = [
349
+ createMappedTask({
350
+ content: 'Important task for specific date',
351
+ labels: ['important'],
352
+ dueDate: '2025-08-15',
353
+ }),
354
+ ];
355
+ const mockResponse = { tasks: mockTasks, nextCursor: null };
356
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
357
+ const result = await findTasksByDate.execute(params, mockTodoistApi);
358
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
359
+ client: mockTodoistApi,
360
+ query: expect.stringContaining('due after:') &&
361
+ expect.stringContaining('(@important)'),
362
+ cursor: undefined,
363
+ limit: 25,
364
+ });
365
+ const textContent = extractTextContent(result);
366
+ expect(textContent).toMatchSnapshot();
367
+ });
368
+ });
250
369
  describe('error handling', () => {
251
370
  it.each([
252
371
  {
@@ -60,7 +60,6 @@ describe(`${FIND_TASKS} tool`, () => {
60
60
  appliedFilters: {
61
61
  searchText: 'important meeting',
62
62
  limit: 10,
63
- cursor: undefined,
64
63
  },
65
64
  }));
66
65
  expect(structuredContent).toEqual(expect.objectContaining({
@@ -70,14 +69,21 @@ describe(`${FIND_TASKS} tool`, () => {
70
69
  it.each([
71
70
  {
72
71
  name: 'custom limit',
73
- params: { searchText: 'project update', limit: 5 },
72
+ params: {
73
+ searchText: 'project update',
74
+ limit: 5,
75
+ },
74
76
  expectedQuery: 'search: project update',
75
77
  expectedLimit: 5,
76
78
  expectedCursor: undefined,
77
79
  },
78
80
  {
79
81
  name: 'pagination cursor',
80
- params: { searchText: 'follow up', limit: 20, cursor: 'cursor-from-first-page' },
82
+ params: {
83
+ searchText: 'follow up',
84
+ limit: 20,
85
+ cursor: 'cursor-from-first-page',
86
+ },
81
87
  expectedQuery: 'search: follow up',
82
88
  expectedLimit: 20,
83
89
  expectedCursor: 'cursor-from-first-page',
@@ -141,26 +147,35 @@ describe(`${FIND_TASKS} tool`, () => {
141
147
  });
142
148
  describe('validation', () => {
143
149
  it('should require at least one filter parameter', async () => {
144
- await expect(findTasks.execute({ limit: 10 }, mockTodoistApi)).rejects.toThrow('At least one filter must be provided: searchText, projectId, sectionId, or parentId');
150
+ await expect(findTasks.execute({ limit: 10 }, mockTodoistApi)).rejects.toThrow('At least one filter must be provided: searchText, projectId, sectionId, parentId, or labels');
145
151
  });
146
152
  });
147
153
  describe('container filtering', () => {
148
154
  it.each([
149
155
  {
150
156
  name: 'project',
151
- params: { projectId: TEST_IDS.PROJECT_TEST, limit: 10 },
157
+ params: {
158
+ projectId: TEST_IDS.PROJECT_TEST,
159
+ limit: 10,
160
+ },
152
161
  expectedApiParam: { projectId: TEST_IDS.PROJECT_TEST },
153
162
  tasks: [createMockTask({ content: 'Project task' })],
154
163
  },
155
164
  {
156
165
  name: 'section',
157
- params: { sectionId: TEST_IDS.SECTION_1, limit: 10 },
166
+ params: {
167
+ sectionId: TEST_IDS.SECTION_1,
168
+ limit: 10,
169
+ },
158
170
  expectedApiParam: { sectionId: TEST_IDS.SECTION_1 },
159
171
  tasks: [createMockTask({ content: 'Section task' })],
160
172
  },
161
173
  {
162
174
  name: 'parent task',
163
- params: { parentId: TEST_IDS.TASK_1, limit: 10 },
175
+ params: {
176
+ parentId: TEST_IDS.TASK_1,
177
+ limit: 10,
178
+ },
164
179
  expectedApiParam: { parentId: TEST_IDS.TASK_1 },
165
180
  tasks: [createMockTask({ content: 'Subtask' })],
166
181
  },
@@ -307,21 +322,253 @@ describe(`${FIND_TASKS} tool`, () => {
307
322
  expect(textContent).toContain('Verify spelling and try partial words');
308
323
  });
309
324
  });
325
+ describe('label filtering', () => {
326
+ it.each([
327
+ {
328
+ name: 'text search with single label OR operator',
329
+ params: {
330
+ searchText: 'important meeting',
331
+ limit: 10,
332
+ labels: ['work'],
333
+ },
334
+ expectedQuery: 'search: important meeting & (@work)',
335
+ },
336
+ {
337
+ name: 'text search with multiple labels AND operator',
338
+ params: {
339
+ searchText: 'project update',
340
+ limit: 15,
341
+ labels: ['work', 'urgent'],
342
+ labelsOperator: 'and',
343
+ },
344
+ expectedQuery: 'search: project update & (@work & @urgent)',
345
+ },
346
+ {
347
+ name: 'text search with multiple labels OR operator',
348
+ params: {
349
+ searchText: 'follow up',
350
+ limit: 20,
351
+ labels: ['personal', 'shopping'],
352
+ },
353
+ expectedQuery: 'search: follow up & (@personal | @shopping)',
354
+ },
355
+ ])('should filter tasks by labels in text search: $name', async ({ params, expectedQuery }) => {
356
+ const mockTasks = [
357
+ createMappedTask({
358
+ id: TEST_IDS.TASK_1,
359
+ content: 'Task with work label',
360
+ labels: ['work'],
361
+ }),
362
+ ];
363
+ const mockResponse = { tasks: mockTasks, nextCursor: null };
364
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
365
+ const result = await findTasks.execute(params, mockTodoistApi);
366
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
367
+ client: mockTodoistApi,
368
+ query: expectedQuery,
369
+ cursor: undefined,
370
+ limit: params.limit,
371
+ });
372
+ const structuredContent = extractStructuredContent(result);
373
+ expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
374
+ searchText: params.searchText,
375
+ labels: params.labels,
376
+ ...(params.labelsOperator ? { labelsOperator: params.labelsOperator } : {}),
377
+ }));
378
+ });
379
+ it.each([
380
+ {
381
+ name: 'project filter with labels',
382
+ params: {
383
+ projectId: TEST_IDS.PROJECT_TEST,
384
+ limit: 10,
385
+ labels: ['important'],
386
+ },
387
+ expectedApiParam: { projectId: TEST_IDS.PROJECT_TEST },
388
+ },
389
+ {
390
+ name: 'section filter with multiple labels',
391
+ params: {
392
+ sectionId: TEST_IDS.SECTION_1,
393
+ limit: 10,
394
+ labels: ['work', 'urgent'],
395
+ labelsOperator: 'and',
396
+ },
397
+ expectedApiParam: { sectionId: TEST_IDS.SECTION_1 },
398
+ },
399
+ {
400
+ name: 'parent task filter with labels',
401
+ params: {
402
+ parentId: TEST_IDS.TASK_1,
403
+ limit: 10,
404
+ labels: ['personal'],
405
+ },
406
+ expectedApiParam: { parentId: TEST_IDS.TASK_1 },
407
+ },
408
+ ])('should apply label filtering to container searches: $name', async ({ params, expectedApiParam }) => {
409
+ const allTasks = [
410
+ createMockTask({
411
+ id: '1',
412
+ content: 'Task with matching label',
413
+ labels: params.labels,
414
+ }),
415
+ createMockTask({
416
+ id: '2',
417
+ content: 'Task without matching label',
418
+ labels: ['other'],
419
+ }),
420
+ ];
421
+ mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(allTasks));
422
+ const result = await findTasks.execute(params, mockTodoistApi);
423
+ expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
424
+ limit: 10,
425
+ cursor: null,
426
+ ...expectedApiParam,
427
+ });
428
+ // Should filter results client-side based on labels
429
+ const structuredContent = extractStructuredContent(result);
430
+ if (params.labelsOperator === 'and') {
431
+ // AND operation: task must have all specified labels
432
+ expect(structuredContent.tasks).toEqual(expect.arrayContaining([
433
+ expect.objectContaining({
434
+ labels: expect.arrayContaining(params.labels),
435
+ }),
436
+ ]));
437
+ }
438
+ else {
439
+ // OR operation: task must have at least one of the specified labels
440
+ expect(structuredContent.tasks.length).toBeGreaterThanOrEqual(0);
441
+ }
442
+ });
443
+ it('should handle empty labels array', async () => {
444
+ const params = {
445
+ searchText: 'test',
446
+ limit: 10,
447
+ };
448
+ const mockResponse = { tasks: [], nextCursor: null };
449
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
450
+ await findTasks.execute(params, mockTodoistApi);
451
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
452
+ client: mockTodoistApi,
453
+ query: 'search: test',
454
+ cursor: undefined,
455
+ limit: 10,
456
+ });
457
+ });
458
+ it('should combine search text, container, and label filters', async () => {
459
+ const params = {
460
+ projectId: TEST_IDS.PROJECT_TEST,
461
+ searchText: 'important',
462
+ limit: 10,
463
+ labels: ['urgent'],
464
+ };
465
+ const allTasks = [
466
+ createMockTask({
467
+ id: '1',
468
+ content: 'important task',
469
+ description: 'urgent work',
470
+ labels: ['urgent'],
471
+ }),
472
+ createMockTask({
473
+ id: '2',
474
+ content: 'other task',
475
+ description: 'not important',
476
+ labels: ['work'],
477
+ }),
478
+ ];
479
+ mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(allTasks));
480
+ const result = await findTasks.execute(params, mockTodoistApi);
481
+ // Should call API with container filter
482
+ expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
483
+ limit: 10,
484
+ cursor: null,
485
+ projectId: TEST_IDS.PROJECT_TEST,
486
+ });
487
+ // Should filter results by search text AND labels
488
+ const structuredContent = extractStructuredContent(result);
489
+ expect(structuredContent.tasks).toEqual([
490
+ expect.objectContaining({
491
+ content: 'important task',
492
+ labels: expect.arrayContaining(['urgent']),
493
+ }),
494
+ ]);
495
+ });
496
+ it('should handle labels-only filtering', async () => {
497
+ const params = {
498
+ limit: 10,
499
+ labels: ['work'],
500
+ };
501
+ const mockTasks = [
502
+ createMappedTask({
503
+ id: TEST_IDS.TASK_1,
504
+ content: 'Task with work label',
505
+ labels: ['work'],
506
+ }),
507
+ ];
508
+ const mockResponse = { tasks: mockTasks, nextCursor: null };
509
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
510
+ const result = await findTasks.execute(params, mockTodoistApi);
511
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
512
+ client: mockTodoistApi,
513
+ query: '(@work)',
514
+ cursor: undefined,
515
+ limit: 10,
516
+ });
517
+ const structuredContent = extractStructuredContent(result);
518
+ expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
519
+ labels: ['work'],
520
+ }));
521
+ });
522
+ it('should handle labels with @ symbol', async () => {
523
+ const params = {
524
+ limit: 10,
525
+ labels: ['@work', 'personal'], // Mix of with and without @
526
+ };
527
+ const mockTasks = [
528
+ createMappedTask({
529
+ id: TEST_IDS.TASK_1,
530
+ content: 'Task with work label',
531
+ labels: ['work', 'personal'],
532
+ }),
533
+ ];
534
+ const mockResponse = { tasks: mockTasks, nextCursor: null };
535
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
536
+ const result = await findTasks.execute(params, mockTodoistApi);
537
+ // Should handle both @work (already has @) and personal (needs @ added)
538
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
539
+ client: mockTodoistApi,
540
+ query: '(@work | @personal)',
541
+ cursor: undefined,
542
+ limit: 10,
543
+ });
544
+ const structuredContent = extractStructuredContent(result);
545
+ expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
546
+ labels: ['@work', 'personal'],
547
+ }));
548
+ });
549
+ });
310
550
  describe('error handling', () => {
311
551
  it.each([
312
552
  {
313
- error: 'At least one filter must be provided: searchText, projectId, sectionId, or parentId',
553
+ error: 'At least one filter must be provided: searchText, projectId, sectionId, parentId, or labels',
314
554
  params: { limit: 10 },
315
555
  expectValidation: true,
316
556
  },
317
557
  {
318
558
  error: TEST_ERRORS.API_RATE_LIMIT,
319
- params: { searchText: 'any search term', limit: 10 },
559
+ params: {
560
+ searchText: 'any search term',
561
+ limit: 10,
562
+ },
320
563
  expectValidation: false,
321
564
  },
322
565
  {
323
566
  error: TEST_ERRORS.INVALID_CURSOR,
324
- params: { searchText: 'test', cursor: 'invalid-cursor-format', limit: 10 },
567
+ params: {
568
+ searchText: 'test',
569
+ cursor: 'invalid-cursor-format',
570
+ limit: 10,
571
+ },
325
572
  expectValidation: false,
326
573
  },
327
574
  ])('should propagate $error', async ({ error, params, expectValidation }) => {
@@ -25,6 +25,7 @@ describe(`${UPDATE_SECTIONS} tool`, () => {
25
25
  isDeleted: false,
26
26
  isCollapsed: false,
27
27
  name: 'Updated Section Name',
28
+ url: 'https://todoist.com/sections/existing-section-123',
28
29
  };
29
30
  mockTodoistApi.updateSection.mockResolvedValue(mockApiResponse);
30
31
  const result = await updateSections.execute({ sections: [{ id: 'existing-section-123', name: 'Updated Section Name' }] }, mockTodoistApi);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=user-info.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-info.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/user-info.test.ts"],"names":[],"mappings":""}