@doist/todoist-ai 4.16.0 → 4.17.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 (178) hide show
  1. package/dist/filter-helpers.d.ts +1 -1
  2. package/dist/index.d.ts +1044 -196
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +61 -81
  5. package/dist/main.js +15 -23
  6. package/dist/mcp-helpers.d.ts +5 -5
  7. package/dist/mcp-helpers.d.ts.map +1 -1
  8. package/dist/mcp-server-BADReNAy.js +3092 -0
  9. package/dist/todoist-tool.d.ts +9 -3
  10. package/dist/todoist-tool.d.ts.map +1 -1
  11. package/dist/tool-helpers.d.ts +1 -1
  12. package/dist/tools/add-comments.d.ts +69 -3
  13. package/dist/tools/add-comments.d.ts.map +1 -1
  14. package/dist/tools/add-projects.d.ts +34 -3
  15. package/dist/tools/add-projects.d.ts.map +1 -1
  16. package/dist/tools/add-sections.d.ts +14 -1
  17. package/dist/tools/add-sections.d.ts.map +1 -1
  18. package/dist/tools/add-tasks.d.ts +65 -10
  19. package/dist/tools/add-tasks.d.ts.map +1 -1
  20. package/dist/tools/complete-tasks.d.ts +20 -1
  21. package/dist/tools/complete-tasks.d.ts.map +1 -1
  22. package/dist/tools/delete-object.d.ts +16 -3
  23. package/dist/tools/delete-object.d.ts.map +1 -1
  24. package/dist/tools/fetch.d.ts +8 -1
  25. package/dist/tools/fetch.d.ts.map +1 -1
  26. package/dist/tools/find-activity.d.ts +44 -7
  27. package/dist/tools/find-activity.d.ts.map +1 -1
  28. package/dist/tools/find-comments.d.ts +69 -3
  29. package/dist/tools/find-comments.d.ts.map +1 -1
  30. package/dist/tools/find-completed-tasks.d.ts +63 -5
  31. package/dist/tools/find-completed-tasks.d.ts.map +1 -1
  32. package/dist/tools/find-project-collaborators.d.ts +33 -2
  33. package/dist/tools/find-project-collaborators.d.ts.map +1 -1
  34. package/dist/tools/find-projects.d.ts +35 -1
  35. package/dist/tools/find-projects.d.ts.map +1 -1
  36. package/dist/tools/find-sections.d.ts +15 -1
  37. package/dist/tools/find-sections.d.ts.map +1 -1
  38. package/dist/tools/find-tasks-by-date.d.ts +61 -3
  39. package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
  40. package/dist/tools/find-tasks.d.ts +63 -5
  41. package/dist/tools/find-tasks.d.ts.map +1 -1
  42. package/dist/tools/get-overview.d.ts +24 -1
  43. package/dist/tools/get-overview.d.ts.map +1 -1
  44. package/dist/tools/manage-assignments.d.ts +39 -2
  45. package/dist/tools/manage-assignments.d.ts.map +1 -1
  46. package/dist/tools/search.d.ts +17 -1
  47. package/dist/tools/search.d.ts.map +1 -1
  48. package/dist/tools/update-comments.d.ts +76 -3
  49. package/dist/tools/update-comments.d.ts.map +1 -1
  50. package/dist/tools/update-projects.d.ts +43 -1
  51. package/dist/tools/update-projects.d.ts.map +1 -1
  52. package/dist/tools/update-sections.d.ts +17 -3
  53. package/dist/tools/update-sections.d.ts.map +1 -1
  54. package/dist/tools/update-tasks.d.ts +79 -13
  55. package/dist/tools/update-tasks.d.ts.map +1 -1
  56. package/dist/tools/user-info.d.ts +19 -1
  57. package/dist/tools/user-info.d.ts.map +1 -1
  58. package/dist/utils/assignment-validator.d.ts +2 -2
  59. package/dist/utils/output-schemas.d.ts +233 -0
  60. package/dist/utils/output-schemas.d.ts.map +1 -0
  61. package/dist/utils/response-builders.d.ts +1 -3
  62. package/dist/utils/response-builders.d.ts.map +1 -1
  63. package/dist/utils/test-helpers.d.ts +1 -1
  64. package/dist/utils/user-resolver.d.ts +1 -1
  65. package/package.json +10 -8
  66. package/dist/filter-helpers.js +0 -79
  67. package/dist/mcp-helpers.js +0 -71
  68. package/dist/mcp-server.js +0 -142
  69. package/dist/todoist-tool.js +0 -1
  70. package/dist/tool-helpers.js +0 -125
  71. package/dist/tool-helpers.test.d.ts +0 -2
  72. package/dist/tool-helpers.test.d.ts.map +0 -1
  73. package/dist/tool-helpers.test.js +0 -223
  74. package/dist/tools/__tests__/add-comments.test.d.ts +0 -2
  75. package/dist/tools/__tests__/add-comments.test.d.ts.map +0 -1
  76. package/dist/tools/__tests__/add-comments.test.js +0 -241
  77. package/dist/tools/__tests__/add-projects.test.d.ts +0 -2
  78. package/dist/tools/__tests__/add-projects.test.d.ts.map +0 -1
  79. package/dist/tools/__tests__/add-projects.test.js +0 -174
  80. package/dist/tools/__tests__/add-sections.test.d.ts +0 -2
  81. package/dist/tools/__tests__/add-sections.test.d.ts.map +0 -1
  82. package/dist/tools/__tests__/add-sections.test.js +0 -185
  83. package/dist/tools/__tests__/add-tasks.test.d.ts +0 -2
  84. package/dist/tools/__tests__/add-tasks.test.d.ts.map +0 -1
  85. package/dist/tools/__tests__/add-tasks.test.js +0 -606
  86. package/dist/tools/__tests__/assignment-integration.test.d.ts +0 -2
  87. package/dist/tools/__tests__/assignment-integration.test.d.ts.map +0 -1
  88. package/dist/tools/__tests__/assignment-integration.test.js +0 -428
  89. package/dist/tools/__tests__/complete-tasks.test.d.ts +0 -2
  90. package/dist/tools/__tests__/complete-tasks.test.d.ts.map +0 -1
  91. package/dist/tools/__tests__/complete-tasks.test.js +0 -206
  92. package/dist/tools/__tests__/delete-object.test.d.ts +0 -2
  93. package/dist/tools/__tests__/delete-object.test.d.ts.map +0 -1
  94. package/dist/tools/__tests__/delete-object.test.js +0 -110
  95. package/dist/tools/__tests__/fetch.test.d.ts +0 -2
  96. package/dist/tools/__tests__/fetch.test.d.ts.map +0 -1
  97. package/dist/tools/__tests__/fetch.test.js +0 -279
  98. package/dist/tools/__tests__/find-activity.test.d.ts +0 -2
  99. package/dist/tools/__tests__/find-activity.test.d.ts.map +0 -1
  100. package/dist/tools/__tests__/find-activity.test.js +0 -229
  101. package/dist/tools/__tests__/find-comments.test.d.ts +0 -2
  102. package/dist/tools/__tests__/find-comments.test.d.ts.map +0 -1
  103. package/dist/tools/__tests__/find-comments.test.js +0 -236
  104. package/dist/tools/__tests__/find-completed-tasks.test.d.ts +0 -2
  105. package/dist/tools/__tests__/find-completed-tasks.test.d.ts.map +0 -1
  106. package/dist/tools/__tests__/find-completed-tasks.test.js +0 -423
  107. package/dist/tools/__tests__/find-projects.test.d.ts +0 -2
  108. package/dist/tools/__tests__/find-projects.test.d.ts.map +0 -1
  109. package/dist/tools/__tests__/find-projects.test.js +0 -154
  110. package/dist/tools/__tests__/find-sections.test.d.ts +0 -2
  111. package/dist/tools/__tests__/find-sections.test.d.ts.map +0 -1
  112. package/dist/tools/__tests__/find-sections.test.js +0 -313
  113. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts +0 -2
  114. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts.map +0 -1
  115. package/dist/tools/__tests__/find-tasks-by-date.test.js +0 -528
  116. package/dist/tools/__tests__/find-tasks.test.d.ts +0 -2
  117. package/dist/tools/__tests__/find-tasks.test.d.ts.map +0 -1
  118. package/dist/tools/__tests__/find-tasks.test.js +0 -771
  119. package/dist/tools/__tests__/get-overview.test.d.ts +0 -2
  120. package/dist/tools/__tests__/get-overview.test.d.ts.map +0 -1
  121. package/dist/tools/__tests__/get-overview.test.js +0 -225
  122. package/dist/tools/__tests__/search.test.d.ts +0 -2
  123. package/dist/tools/__tests__/search.test.d.ts.map +0 -1
  124. package/dist/tools/__tests__/search.test.js +0 -206
  125. package/dist/tools/__tests__/update-comments.test.d.ts +0 -2
  126. package/dist/tools/__tests__/update-comments.test.d.ts.map +0 -1
  127. package/dist/tools/__tests__/update-comments.test.js +0 -294
  128. package/dist/tools/__tests__/update-projects.test.d.ts +0 -2
  129. package/dist/tools/__tests__/update-projects.test.d.ts.map +0 -1
  130. package/dist/tools/__tests__/update-projects.test.js +0 -217
  131. package/dist/tools/__tests__/update-sections.test.d.ts +0 -2
  132. package/dist/tools/__tests__/update-sections.test.d.ts.map +0 -1
  133. package/dist/tools/__tests__/update-sections.test.js +0 -169
  134. package/dist/tools/__tests__/update-tasks.test.d.ts +0 -2
  135. package/dist/tools/__tests__/update-tasks.test.d.ts.map +0 -1
  136. package/dist/tools/__tests__/update-tasks.test.js +0 -788
  137. package/dist/tools/__tests__/user-info.test.d.ts +0 -2
  138. package/dist/tools/__tests__/user-info.test.d.ts.map +0 -1
  139. package/dist/tools/__tests__/user-info.test.js +0 -139
  140. package/dist/tools/add-comments.js +0 -89
  141. package/dist/tools/add-projects.js +0 -63
  142. package/dist/tools/add-sections.js +0 -74
  143. package/dist/tools/add-tasks.js +0 -169
  144. package/dist/tools/complete-tasks.js +0 -68
  145. package/dist/tools/delete-object.js +0 -79
  146. package/dist/tools/fetch.js +0 -102
  147. package/dist/tools/find-activity.js +0 -221
  148. package/dist/tools/find-comments.js +0 -148
  149. package/dist/tools/find-completed-tasks.js +0 -168
  150. package/dist/tools/find-project-collaborators.js +0 -151
  151. package/dist/tools/find-projects.js +0 -101
  152. package/dist/tools/find-sections.js +0 -101
  153. package/dist/tools/find-tasks-by-date.js +0 -198
  154. package/dist/tools/find-tasks.js +0 -329
  155. package/dist/tools/get-overview.js +0 -249
  156. package/dist/tools/manage-assignments.js +0 -337
  157. package/dist/tools/search.js +0 -65
  158. package/dist/tools/update-comments.js +0 -82
  159. package/dist/tools/update-projects.js +0 -84
  160. package/dist/tools/update-sections.js +0 -70
  161. package/dist/tools/update-tasks.js +0 -179
  162. package/dist/tools/user-info.js +0 -142
  163. package/dist/utils/assignment-validator.js +0 -253
  164. package/dist/utils/constants.js +0 -45
  165. package/dist/utils/duration-parser.js +0 -96
  166. package/dist/utils/duration-parser.test.d.ts +0 -2
  167. package/dist/utils/duration-parser.test.d.ts.map +0 -1
  168. package/dist/utils/duration-parser.test.js +0 -147
  169. package/dist/utils/labels.js +0 -18
  170. package/dist/utils/priorities.js +0 -20
  171. package/dist/utils/response-builders.js +0 -210
  172. package/dist/utils/sanitize-data.js +0 -37
  173. package/dist/utils/sanitize-data.test.d.ts +0 -2
  174. package/dist/utils/sanitize-data.test.d.ts.map +0 -1
  175. package/dist/utils/sanitize-data.test.js +0 -93
  176. package/dist/utils/test-helpers.js +0 -237
  177. package/dist/utils/tool-names.js +0 -40
  178. package/dist/utils/user-resolver.js +0 -179
@@ -1,771 +0,0 @@
1
- import { jest } from '@jest/globals';
2
- import { getTasksByFilter } from '../../tool-helpers.js';
3
- import { createMappedTask, createMockApiResponse, createMockTask, createMockUser, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, TODAY, } from '../../utils/test-helpers.js';
4
- import { ToolNames } from '../../utils/tool-names.js';
5
- import { resolveUserNameToId } from '../../utils/user-resolver.js';
6
- import { findTasks } from '../find-tasks.js';
7
- jest.mock('../../tool-helpers', () => {
8
- const actual = jest.requireActual('../../tool-helpers');
9
- return {
10
- getTasksByFilter: jest.fn(),
11
- mapTask: actual.mapTask,
12
- filterTasksByResponsibleUser: actual.filterTasksByResponsibleUser,
13
- };
14
- });
15
- jest.mock('../../utils/user-resolver', () => ({
16
- resolveUserNameToId: jest.fn(),
17
- }));
18
- const { FIND_TASKS, UPDATE_TASKS, FIND_COMPLETED_TASKS } = ToolNames;
19
- const mockGetTasksByFilter = getTasksByFilter;
20
- const mockResolveUserNameToId = resolveUserNameToId;
21
- // Mock the Todoist API
22
- const mockTodoistApi = {
23
- getTasks: jest.fn(),
24
- getUser: jest.fn(),
25
- };
26
- // Mock the Todoist User
27
- const mockTodoistUser = createMockUser();
28
- describe(`${FIND_TASKS} tool`, () => {
29
- beforeEach(() => {
30
- mockTodoistApi.getUser.mockResolvedValue(mockTodoistUser);
31
- jest.clearAllMocks();
32
- });
33
- describe('searching tasks', () => {
34
- it('should search tasks and return results', async () => {
35
- const mockTasks = [
36
- createMappedTask({
37
- id: TEST_IDS.TASK_1,
38
- content: 'Task containing search term',
39
- description: 'Description with more details',
40
- labels: ['work'],
41
- }),
42
- createMappedTask({
43
- id: TEST_IDS.TASK_2,
44
- content: 'Another matching task',
45
- priority: 2,
46
- sectionId: TEST_IDS.SECTION_1,
47
- }),
48
- ];
49
- const mockResponse = { tasks: mockTasks, nextCursor: 'cursor-for-next-page' };
50
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
51
- const result = await findTasks.execute({
52
- searchText: 'important meeting',
53
- limit: 10,
54
- }, mockTodoistApi);
55
- expect(mockGetTasksByFilter).toHaveBeenCalledWith({
56
- client: mockTodoistApi,
57
- query: 'search: important meeting',
58
- cursor: undefined,
59
- limit: 10,
60
- });
61
- // Verify result is a concise summary
62
- expect(extractTextContent(result)).toMatchSnapshot();
63
- // Verify structured content
64
- const structuredContent = extractStructuredContent(result);
65
- expect(structuredContent).toEqual(expect.objectContaining({
66
- tasks: expect.any(Array),
67
- totalCount: 2,
68
- hasMore: true,
69
- nextCursor: 'cursor-for-next-page',
70
- appliedFilters: {
71
- searchText: 'important meeting',
72
- limit: 10,
73
- },
74
- }));
75
- expect(structuredContent).toEqual(expect.objectContaining({
76
- tasks: expect.any(Array),
77
- }));
78
- });
79
- it.each([
80
- {
81
- name: 'custom limit',
82
- params: {
83
- searchText: 'project update',
84
- limit: 5,
85
- },
86
- expectedQuery: 'search: project update',
87
- expectedLimit: 5,
88
- expectedCursor: undefined,
89
- },
90
- {
91
- name: 'pagination cursor',
92
- params: {
93
- searchText: 'follow up',
94
- limit: 20,
95
- cursor: 'cursor-from-first-page',
96
- },
97
- expectedQuery: 'search: follow up',
98
- expectedLimit: 20,
99
- expectedCursor: 'cursor-from-first-page',
100
- },
101
- ])('should handle $name', async ({ params, expectedQuery, expectedLimit, expectedCursor }) => {
102
- const mockTask = createMappedTask({ content: 'Test result' });
103
- const mockResponse = { tasks: [mockTask], nextCursor: null };
104
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
105
- const result = await findTasks.execute(params, mockTodoistApi);
106
- expect(mockGetTasksByFilter).toHaveBeenCalledWith({
107
- client: mockTodoistApi,
108
- query: expectedQuery,
109
- cursor: expectedCursor,
110
- limit: expectedLimit,
111
- });
112
- // Verify result is a concise summary
113
- expect(extractTextContent(result)).toMatchSnapshot();
114
- // Verify structured content
115
- const structuredContent = extractStructuredContent(result);
116
- expect(structuredContent).toEqual(expect.objectContaining({
117
- tasks: expect.any(Array),
118
- totalCount: 1,
119
- hasMore: false,
120
- appliedFilters: expect.objectContaining({
121
- searchText: params.searchText,
122
- limit: expectedLimit,
123
- }),
124
- }));
125
- expect(structuredContent).toEqual(expect.objectContaining({
126
- tasks: expect.any(Array),
127
- }));
128
- });
129
- it.each([
130
- { searchText: '@work #urgent "exact phrase"', description: 'special characters' },
131
- { searchText: 'nonexistent keyword', description: 'empty results' },
132
- ])('should handle search with $description', async ({ searchText }) => {
133
- const mockResponse = { tasks: [], nextCursor: null };
134
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
135
- const result = await findTasks.execute({ searchText, limit: 10 }, mockTodoistApi);
136
- expect(mockGetTasksByFilter).toHaveBeenCalledWith({
137
- client: mockTodoistApi,
138
- query: `search: ${searchText}`,
139
- cursor: undefined,
140
- limit: 10,
141
- });
142
- // Verify result is a concise summary
143
- expect(extractTextContent(result)).toMatchSnapshot();
144
- // Verify structured content for empty results
145
- const structuredContent = extractStructuredContent(result);
146
- expect(structuredContent).toEqual({
147
- // tasks array is removed when empty
148
- totalCount: 0,
149
- hasMore: false,
150
- appliedFilters: expect.objectContaining({
151
- searchText: searchText,
152
- }),
153
- });
154
- });
155
- });
156
- describe('validation', () => {
157
- it('should require at least one filter parameter', async () => {
158
- await expect(findTasks.execute({ limit: 10 }, mockTodoistApi)).rejects.toThrow('At least one filter must be provided: searchText, projectId, sectionId, parentId, responsibleUser, or labels');
159
- });
160
- });
161
- describe('container filtering', () => {
162
- it.each([
163
- {
164
- name: 'project',
165
- params: {
166
- projectId: TEST_IDS.PROJECT_TEST,
167
- limit: 10,
168
- },
169
- expectedApiParam: { projectId: TEST_IDS.PROJECT_TEST },
170
- tasks: [createMockTask({ content: 'Project task' })],
171
- },
172
- {
173
- name: 'section',
174
- params: {
175
- sectionId: TEST_IDS.SECTION_1,
176
- limit: 10,
177
- },
178
- expectedApiParam: { sectionId: TEST_IDS.SECTION_1 },
179
- tasks: [createMockTask({ content: 'Section task' })],
180
- },
181
- {
182
- name: 'parent task',
183
- params: {
184
- parentId: TEST_IDS.TASK_1,
185
- limit: 10,
186
- },
187
- expectedApiParam: { parentId: TEST_IDS.TASK_1 },
188
- tasks: [createMockTask({ content: 'Subtask' })],
189
- },
190
- ])('should find tasks in $name', async ({ params, expectedApiParam, tasks }) => {
191
- mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(tasks));
192
- const result = await findTasks.execute(params, mockTodoistApi);
193
- expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
194
- limit: 10,
195
- cursor: null,
196
- ...expectedApiParam,
197
- });
198
- expect(extractTextContent(result)).toMatchSnapshot();
199
- const structuredContent = extractStructuredContent(result);
200
- expect(structuredContent).toEqual(expect.objectContaining({
201
- tasks: expect.any(Array),
202
- totalCount: tasks.length,
203
- hasMore: false,
204
- appliedFilters: params,
205
- }));
206
- });
207
- it('should handle combined search text and container filtering', async () => {
208
- const tasks = [
209
- createMockTask({
210
- id: '8485093749',
211
- content: 'relevant task',
212
- description: 'contains search term',
213
- }),
214
- createMockTask({
215
- id: '8485093750',
216
- content: 'other task',
217
- description: 'different content',
218
- }),
219
- ];
220
- mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(tasks));
221
- const result = await findTasks.execute({
222
- projectId: TEST_IDS.PROJECT_TEST,
223
- searchText: 'relevant',
224
- limit: 10,
225
- }, mockTodoistApi);
226
- expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
227
- limit: 10,
228
- cursor: null,
229
- projectId: TEST_IDS.PROJECT_TEST,
230
- });
231
- const structuredContent = extractStructuredContent(result);
232
- expect(structuredContent.tasks).toHaveLength(1);
233
- expect(structuredContent.tasks).toEqual([
234
- expect.objectContaining({ content: 'relevant task' }),
235
- ]);
236
- });
237
- it('should handle empty containers', async () => {
238
- mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse([]));
239
- const result = await findTasks.execute({
240
- sectionId: 'empty-section',
241
- limit: 10,
242
- }, mockTodoistApi);
243
- const textContent = extractTextContent(result);
244
- expect(textContent).toContain('Section is empty');
245
- expect(textContent).toContain('Tasks may be in other sections of the project');
246
- });
247
- it('should handle pagination with containers', async () => {
248
- mockTodoistApi.getTasks.mockResolvedValue({
249
- results: [],
250
- nextCursor: 'next-cursor',
251
- });
252
- const result = await findTasks.execute({
253
- projectId: TEST_IDS.PROJECT_TEST,
254
- limit: 25,
255
- cursor: 'current-cursor',
256
- }, mockTodoistApi);
257
- expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
258
- limit: 25,
259
- cursor: 'current-cursor',
260
- projectId: TEST_IDS.PROJECT_TEST,
261
- });
262
- const structuredContent = extractStructuredContent(result);
263
- expect(structuredContent.hasMore).toBe(true);
264
- expect(structuredContent.nextCursor).toBe('next-cursor');
265
- });
266
- });
267
- describe('container error handling', () => {
268
- it('should propagate API errors for container queries', async () => {
269
- const apiError = new Error('API Error: Project not found');
270
- mockTodoistApi.getTasks.mockRejectedValue(apiError);
271
- await expect(findTasks.execute({ projectId: 'non-existent', limit: 10 }, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
272
- });
273
- });
274
- describe('next steps logic', () => {
275
- it('should suggest different actions when hasOverdue is true', async () => {
276
- const mockTasks = [
277
- createMappedTask({
278
- id: TEST_IDS.TASK_1,
279
- content: 'Overdue search result',
280
- dueDate: '2025-08-10', // Past date
281
- }),
282
- ];
283
- const mockResponse = { tasks: mockTasks, nextCursor: null };
284
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
285
- const result = await findTasks.execute({ searchText: 'overdue tasks', limit: 10 }, mockTodoistApi);
286
- const textContent = extractTextContent(result);
287
- expect(textContent).toMatchSnapshot();
288
- // Should prioritize overdue tasks in next steps
289
- expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
290
- });
291
- it('should suggest today tasks when hasToday is true', async () => {
292
- const mockTasks = [
293
- createMappedTask({
294
- id: TEST_IDS.TASK_1,
295
- content: 'Task due today',
296
- dueDate: TODAY,
297
- }),
298
- ];
299
- const mockResponse = { tasks: mockTasks, nextCursor: null };
300
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
301
- const result = await findTasks.execute({ searchText: 'today tasks', limit: 10 }, mockTodoistApi);
302
- const textContent = extractTextContent(result);
303
- expect(textContent).toMatchSnapshot();
304
- // Should suggest today-focused actions
305
- expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
306
- });
307
- it('should provide different next steps for regular tasks', async () => {
308
- const mockTasks = [
309
- createMappedTask({
310
- id: TEST_IDS.TASK_1,
311
- content: 'Regular future task',
312
- dueDate: '2025-08-25', // Future date
313
- }),
314
- ];
315
- const mockResponse = { tasks: mockTasks, nextCursor: null };
316
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
317
- const result = await findTasks.execute({ searchText: 'future tasks', limit: 10 }, mockTodoistApi);
318
- const textContent = extractTextContent(result);
319
- expect(textContent).toMatchSnapshot();
320
- expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
321
- });
322
- it('should provide helpful suggestions for empty search results', async () => {
323
- const mockResponse = { tasks: [], nextCursor: null };
324
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
325
- const result = await findTasks.execute({ searchText: 'nonexistent', limit: 10 }, mockTodoistApi);
326
- const textContent = extractTextContent(result);
327
- expect(textContent).toMatchSnapshot();
328
- expect(textContent).toContain('Try broader search terms');
329
- expect(textContent).toContain(`Check completed tasks with ${FIND_COMPLETED_TASKS}`);
330
- expect(textContent).toContain('Verify spelling and try partial words');
331
- });
332
- });
333
- describe('label filtering', () => {
334
- it.each([
335
- {
336
- name: 'text search with single label OR operator',
337
- params: {
338
- searchText: 'important meeting',
339
- limit: 10,
340
- labels: ['work'],
341
- },
342
- expectedQuery: 'search: important meeting & (@work)',
343
- },
344
- {
345
- name: 'text search with multiple labels AND operator',
346
- params: {
347
- searchText: 'project update',
348
- limit: 15,
349
- labels: ['work', 'urgent'],
350
- labelsOperator: 'and',
351
- },
352
- expectedQuery: 'search: project update & (@work & @urgent)',
353
- },
354
- {
355
- name: 'text search with multiple labels OR operator',
356
- params: {
357
- searchText: 'follow up',
358
- limit: 20,
359
- labels: ['personal', 'shopping'],
360
- },
361
- expectedQuery: 'search: follow up & (@personal | @shopping)',
362
- },
363
- ])('should filter tasks by labels in text search: $name', async ({ params, expectedQuery }) => {
364
- const mockTasks = [
365
- createMappedTask({
366
- id: TEST_IDS.TASK_1,
367
- content: 'Task with work label',
368
- labels: ['work'],
369
- }),
370
- ];
371
- const mockResponse = { tasks: mockTasks, nextCursor: null };
372
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
373
- const result = await findTasks.execute(params, mockTodoistApi);
374
- expect(mockGetTasksByFilter).toHaveBeenCalledWith({
375
- client: mockTodoistApi,
376
- query: expectedQuery,
377
- cursor: undefined,
378
- limit: params.limit,
379
- });
380
- const structuredContent = extractStructuredContent(result);
381
- expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
382
- searchText: params.searchText,
383
- labels: params.labels,
384
- ...(params.labelsOperator ? { labelsOperator: params.labelsOperator } : {}),
385
- }));
386
- });
387
- it.each([
388
- {
389
- name: 'project filter with labels',
390
- params: {
391
- projectId: TEST_IDS.PROJECT_TEST,
392
- limit: 10,
393
- labels: ['important'],
394
- },
395
- expectedApiParam: { projectId: TEST_IDS.PROJECT_TEST },
396
- },
397
- {
398
- name: 'section filter with multiple labels',
399
- params: {
400
- sectionId: TEST_IDS.SECTION_1,
401
- limit: 10,
402
- labels: ['work', 'urgent'],
403
- labelsOperator: 'and',
404
- },
405
- expectedApiParam: { sectionId: TEST_IDS.SECTION_1 },
406
- },
407
- {
408
- name: 'parent task filter with labels',
409
- params: {
410
- parentId: TEST_IDS.TASK_1,
411
- limit: 10,
412
- labels: ['personal'],
413
- },
414
- expectedApiParam: { parentId: TEST_IDS.TASK_1 },
415
- },
416
- ])('should apply label filtering to container searches: $name', async ({ params, expectedApiParam }) => {
417
- const allTasks = [
418
- createMockTask({
419
- id: '1',
420
- content: 'Task with matching label',
421
- labels: params.labels,
422
- }),
423
- createMockTask({
424
- id: '2',
425
- content: 'Task without matching label',
426
- labels: ['other'],
427
- }),
428
- ];
429
- mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(allTasks));
430
- const result = await findTasks.execute(params, mockTodoistApi);
431
- expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
432
- limit: 10,
433
- cursor: null,
434
- ...expectedApiParam,
435
- });
436
- // Should filter results client-side based on labels
437
- const structuredContent = extractStructuredContent(result);
438
- if (params.labelsOperator === 'and') {
439
- // AND operation: task must have all specified labels
440
- expect(structuredContent.tasks).toEqual(expect.arrayContaining([
441
- expect.objectContaining({
442
- labels: expect.arrayContaining(params.labels),
443
- }),
444
- ]));
445
- }
446
- else {
447
- // OR operation: task must have at least one of the specified labels
448
- expect(structuredContent.tasks.length).toBeGreaterThanOrEqual(0);
449
- }
450
- });
451
- it('should handle empty labels array', async () => {
452
- const params = {
453
- searchText: 'test',
454
- limit: 10,
455
- };
456
- const mockResponse = { tasks: [], nextCursor: null };
457
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
458
- await findTasks.execute(params, mockTodoistApi);
459
- expect(mockGetTasksByFilter).toHaveBeenCalledWith({
460
- client: mockTodoistApi,
461
- query: 'search: test',
462
- cursor: undefined,
463
- limit: 10,
464
- });
465
- });
466
- it('should combine search text, container, and label filters', async () => {
467
- const params = {
468
- projectId: TEST_IDS.PROJECT_TEST,
469
- searchText: 'important',
470
- limit: 10,
471
- labels: ['urgent'],
472
- };
473
- const allTasks = [
474
- createMockTask({
475
- id: '1',
476
- content: 'important task',
477
- description: 'urgent work',
478
- labels: ['urgent'],
479
- }),
480
- createMockTask({
481
- id: '2',
482
- content: 'other task',
483
- description: 'not important',
484
- labels: ['work'],
485
- }),
486
- ];
487
- mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(allTasks));
488
- const result = await findTasks.execute(params, mockTodoistApi);
489
- // Should call API with container filter
490
- expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
491
- limit: 10,
492
- cursor: null,
493
- projectId: TEST_IDS.PROJECT_TEST,
494
- });
495
- // Should filter results by search text AND labels
496
- const structuredContent = extractStructuredContent(result);
497
- expect(structuredContent.tasks).toEqual([
498
- expect.objectContaining({
499
- content: 'important task',
500
- labels: expect.arrayContaining(['urgent']),
501
- }),
502
- ]);
503
- });
504
- it('should handle labels-only filtering', async () => {
505
- const params = {
506
- limit: 10,
507
- labels: ['work'],
508
- };
509
- const mockTasks = [
510
- createMappedTask({
511
- id: TEST_IDS.TASK_1,
512
- content: 'Task with work label',
513
- labels: ['work'],
514
- }),
515
- ];
516
- const mockResponse = { tasks: mockTasks, nextCursor: null };
517
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
518
- const result = await findTasks.execute(params, mockTodoistApi);
519
- expect(mockGetTasksByFilter).toHaveBeenCalledWith({
520
- client: mockTodoistApi,
521
- query: '(@work)',
522
- cursor: undefined,
523
- limit: 10,
524
- });
525
- const structuredContent = extractStructuredContent(result);
526
- expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
527
- labels: ['work'],
528
- }));
529
- });
530
- it('should handle labels with @ symbol', async () => {
531
- const params = {
532
- limit: 10,
533
- labels: ['@work', 'personal'], // Mix of with and without @
534
- };
535
- const mockTasks = [
536
- createMappedTask({
537
- id: TEST_IDS.TASK_1,
538
- content: 'Task with work label',
539
- labels: ['work', 'personal'],
540
- }),
541
- ];
542
- const mockResponse = { tasks: mockTasks, nextCursor: null };
543
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
544
- const result = await findTasks.execute(params, mockTodoistApi);
545
- // Should handle both @work (already has @) and personal (needs @ added)
546
- expect(mockGetTasksByFilter).toHaveBeenCalledWith({
547
- client: mockTodoistApi,
548
- query: '(@work | @personal)',
549
- cursor: undefined,
550
- limit: 10,
551
- });
552
- const structuredContent = extractStructuredContent(result);
553
- expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
554
- labels: ['@work', 'personal'],
555
- }));
556
- });
557
- });
558
- describe('markdown content preservation', () => {
559
- it('should preserve markdown links and formatting in task content and description', async () => {
560
- const richMarkdownContent = 'Test **bold** task with [link](https://example.com)';
561
- const richMarkdownDescription = `This is a **comprehensive test** of markdown syntax in Todoist task descriptions:
562
-
563
- ### Links
564
- [Wikipedia - Test Link](https://en.wikipedia.org/wiki/Test)
565
- [GitHub Repository](https://github.com/Doist/todoist-ai)
566
- [Google Search](https://www.google.com)
567
-
568
- ### Text Formatting
569
- **Bold text here**
570
- *Italic text here*
571
- ***Bold and italic***
572
- \`inline code\`
573
-
574
- ### Lists
575
- - Bullet point 1
576
- - Bullet point 2
577
- - Nested item
578
-
579
- 1. Numbered item 1
580
- 2. Numbered item 2
581
-
582
- End of test content.`;
583
- const mockTasks = [
584
- createMappedTask({
585
- id: TEST_IDS.TASK_1,
586
- content: richMarkdownContent,
587
- description: richMarkdownDescription,
588
- }),
589
- ];
590
- const mockResponse = { tasks: mockTasks, nextCursor: null };
591
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
592
- const result = await findTasks.execute({ searchText: 'markdown test', limit: 10 }, mockTodoistApi);
593
- const structuredContent = extractStructuredContent(result);
594
- // Verify that markdown links and formatting are preserved exactly as provided
595
- expect(structuredContent.tasks).toHaveLength(1);
596
- expect(structuredContent.tasks).toEqual(expect.arrayContaining([
597
- expect.objectContaining({
598
- content: richMarkdownContent,
599
- description: richMarkdownDescription,
600
- }),
601
- ]));
602
- });
603
- it('should preserve markdown links in container-based searches', async () => {
604
- const taskWithLinks = createMockTask({
605
- id: TEST_IDS.TASK_1,
606
- content: 'Task with [external link](https://todoist.com)',
607
- description: 'See this [documentation](https://docs.example.com) for details.',
608
- });
609
- mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse([taskWithLinks]));
610
- const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_TEST, limit: 10 }, mockTodoistApi);
611
- const structuredContent = extractStructuredContent(result);
612
- // Verify URLs are preserved in container-based searches too
613
- expect(structuredContent.tasks).toHaveLength(1);
614
- expect(structuredContent.tasks).toEqual(expect.arrayContaining([
615
- expect.objectContaining({
616
- content: 'Task with [external link](https://todoist.com)',
617
- description: 'See this [documentation](https://docs.example.com) for details.',
618
- }),
619
- ]));
620
- });
621
- });
622
- describe('responsible user filtering', () => {
623
- describe('when no responsibleUser is provided', () => {
624
- it('should filter text search results to show only unassigned tasks or tasks assigned to current user', async () => {
625
- const mockTasks = [
626
- createMappedTask({
627
- id: TEST_IDS.TASK_1,
628
- content: 'My task',
629
- responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
630
- }),
631
- createMappedTask({
632
- id: TEST_IDS.TASK_2,
633
- content: 'Unassigned task',
634
- responsibleUid: null, // Unassigned
635
- }),
636
- createMappedTask({
637
- id: TEST_IDS.TASK_3,
638
- content: 'Someone else task',
639
- responsibleUid: 'other-user-id', // Assigned to someone else
640
- }),
641
- ];
642
- const mockResponse = { tasks: mockTasks, nextCursor: null };
643
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
644
- const result = await findTasks.execute({ searchText: 'task', limit: 10 }, mockTodoistApi);
645
- const structuredContent = extractStructuredContent(result);
646
- // Should only return tasks 1 and 2, not task 3
647
- expect(structuredContent.tasks).toHaveLength(2);
648
- expect(structuredContent.tasks.map((t) => t.id)).toEqual([TEST_IDS.TASK_1, TEST_IDS.TASK_2]);
649
- });
650
- it('should filter container-based results to show only unassigned tasks or tasks assigned to current user', async () => {
651
- const mockTasks = [
652
- createMockTask({
653
- id: TEST_IDS.TASK_1,
654
- content: 'My project task',
655
- responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
656
- }),
657
- createMockTask({
658
- id: TEST_IDS.TASK_2,
659
- content: 'Unassigned project task',
660
- responsibleUid: null, // Unassigned
661
- }),
662
- createMockTask({
663
- id: TEST_IDS.TASK_3,
664
- content: 'Someone else project task',
665
- responsibleUid: 'other-user-id', // Assigned to someone else
666
- }),
667
- ];
668
- mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(mockTasks));
669
- const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_WORK, limit: 10 }, mockTodoistApi);
670
- const structuredContent = extractStructuredContent(result);
671
- // Should only return tasks 1 and 2, not task 3
672
- expect(structuredContent.tasks).toHaveLength(2);
673
- expect(structuredContent.tasks.map((t) => t.id)).toEqual([TEST_IDS.TASK_1, TEST_IDS.TASK_2]);
674
- });
675
- });
676
- describe('when responsibleUser is provided', () => {
677
- it('should filter text search results to show only tasks assigned to specified user', async () => {
678
- const mockTasks = [
679
- createMappedTask({
680
- id: TEST_IDS.TASK_1,
681
- content: 'Task for John',
682
- responsibleUid: 'specific-user-id', // Assigned to specified user
683
- }),
684
- createMappedTask({
685
- id: TEST_IDS.TASK_2,
686
- content: 'My task',
687
- responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
688
- }),
689
- createMappedTask({
690
- id: TEST_IDS.TASK_3,
691
- content: 'Unassigned task',
692
- responsibleUid: null, // Unassigned
693
- }),
694
- ];
695
- const mockResponse = { tasks: mockTasks, nextCursor: null };
696
- mockGetTasksByFilter.mockResolvedValue(mockResponse);
697
- mockResolveUserNameToId.mockResolvedValue({
698
- userId: 'specific-user-id',
699
- displayName: 'John Doe',
700
- email: 'john@example.com',
701
- });
702
- const result = await findTasks.execute({ searchText: 'task', responsibleUser: 'John Doe', limit: 10 }, mockTodoistApi);
703
- const structuredContent = extractStructuredContent(result);
704
- // Should only return task 1 (assigned to John)
705
- expect(structuredContent.tasks).toHaveLength(1);
706
- expect(structuredContent.tasks[0]?.id).toBe(TEST_IDS.TASK_1);
707
- });
708
- it('should filter container-based results to show only tasks assigned to specified user', async () => {
709
- const mockTasks = [
710
- createMockTask({
711
- id: TEST_IDS.TASK_1,
712
- content: 'Task for John',
713
- responsibleUid: 'specific-user-id', // Assigned to specified user
714
- }),
715
- createMockTask({
716
- id: TEST_IDS.TASK_2,
717
- content: 'My task',
718
- responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
719
- }),
720
- createMockTask({
721
- id: TEST_IDS.TASK_3,
722
- content: 'Unassigned task',
723
- responsibleUid: null, // Unassigned
724
- }),
725
- ];
726
- mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(mockTasks));
727
- mockResolveUserNameToId.mockResolvedValue({
728
- userId: 'specific-user-id',
729
- displayName: 'John Doe',
730
- email: 'john@example.com',
731
- });
732
- const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_WORK, responsibleUser: 'John Doe', limit: 10 }, mockTodoistApi);
733
- const structuredContent = extractStructuredContent(result);
734
- // Should only return task 1 (assigned to John)
735
- expect(structuredContent.tasks).toHaveLength(1);
736
- expect(structuredContent.tasks[0]?.id).toBe(TEST_IDS.TASK_1);
737
- });
738
- });
739
- });
740
- describe('error handling', () => {
741
- it.each([
742
- {
743
- error: 'At least one filter must be provided: searchText, projectId, sectionId, parentId, responsibleUser, or labels',
744
- params: { limit: 10 },
745
- expectValidation: true,
746
- },
747
- {
748
- error: TEST_ERRORS.API_RATE_LIMIT,
749
- params: {
750
- searchText: 'any search term',
751
- limit: 10,
752
- },
753
- expectValidation: false,
754
- },
755
- {
756
- error: TEST_ERRORS.INVALID_CURSOR,
757
- params: {
758
- searchText: 'test',
759
- cursor: 'invalid-cursor-format',
760
- limit: 10,
761
- },
762
- expectValidation: false,
763
- },
764
- ])('should propagate $error', async ({ error, params, expectValidation }) => {
765
- if (!expectValidation) {
766
- mockGetTasksByFilter.mockRejectedValue(new Error(error));
767
- }
768
- await expect(findTasks.execute(params, mockTodoistApi)).rejects.toThrow(error);
769
- });
770
- });
771
- });