@doist/todoist-ai 2.2.2 → 3.0.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 (168) hide show
  1. package/README.md +11 -3
  2. package/dist/index.d.ts +496 -255
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +41 -29
  5. package/dist/mcp-helpers.d.ts +25 -3
  6. package/dist/mcp-helpers.d.ts.map +1 -1
  7. package/dist/mcp-helpers.js +37 -19
  8. package/dist/mcp-server.d.ts.map +1 -1
  9. package/dist/mcp-server.js +32 -28
  10. package/dist/tools/__tests__/add-tasks.test.d.ts +2 -0
  11. package/dist/tools/__tests__/add-tasks.test.d.ts.map +1 -0
  12. package/dist/tools/__tests__/{tasks-add-multiple.test.js → add-tasks.test.js} +85 -81
  13. package/dist/tools/__tests__/complete-tasks.test.d.ts +2 -0
  14. package/dist/tools/__tests__/complete-tasks.test.d.ts.map +1 -0
  15. package/dist/tools/__tests__/complete-tasks.test.js +206 -0
  16. package/dist/tools/__tests__/delete-object.test.d.ts +2 -0
  17. package/dist/tools/__tests__/delete-object.test.d.ts.map +1 -0
  18. package/dist/tools/__tests__/{delete-one.test.js → delete-object.test.js} +42 -22
  19. package/dist/tools/__tests__/find-completed-tasks.test.d.ts +2 -0
  20. package/dist/tools/__tests__/find-completed-tasks.test.d.ts.map +1 -0
  21. package/dist/tools/__tests__/{tasks-list-completed.test.js → find-completed-tasks.test.js} +13 -36
  22. package/dist/tools/__tests__/find-projects.test.d.ts +2 -0
  23. package/dist/tools/__tests__/find-projects.test.d.ts.map +1 -0
  24. package/dist/tools/__tests__/{projects-list.test.js → find-projects.test.js} +55 -39
  25. package/dist/tools/__tests__/find-sections.test.d.ts +2 -0
  26. package/dist/tools/__tests__/find-sections.test.d.ts.map +1 -0
  27. package/dist/tools/__tests__/{sections-search.test.js → find-sections.test.js} +64 -50
  28. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts +2 -0
  29. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts.map +1 -0
  30. package/dist/tools/__tests__/{tasks-list-by-date.test.js → find-tasks-by-date.test.js} +96 -14
  31. package/dist/tools/__tests__/find-tasks.test.d.ts +2 -0
  32. package/dist/tools/__tests__/find-tasks.test.d.ts.map +1 -0
  33. package/dist/tools/__tests__/find-tasks.test.js +334 -0
  34. package/dist/tools/__tests__/get-overview.test.d.ts +2 -0
  35. package/dist/tools/__tests__/get-overview.test.d.ts.map +1 -0
  36. package/dist/tools/__tests__/{overview.test.js → get-overview.test.js} +77 -13
  37. package/dist/tools/__tests__/manage-projects.test.d.ts +2 -0
  38. package/dist/tools/__tests__/manage-projects.test.d.ts.map +1 -0
  39. package/dist/tools/__tests__/{projects-manage.test.js → manage-projects.test.js} +33 -30
  40. package/dist/tools/__tests__/manage-sections.test.d.ts +2 -0
  41. package/dist/tools/__tests__/manage-sections.test.d.ts.map +1 -0
  42. package/dist/tools/__tests__/manage-sections.test.js +162 -0
  43. package/dist/tools/__tests__/update-tasks.test.d.ts +2 -0
  44. package/dist/tools/__tests__/update-tasks.test.d.ts.map +1 -0
  45. package/dist/tools/__tests__/update-tasks.test.js +645 -0
  46. package/dist/tools/{tasks-add-multiple.d.ts → add-tasks.d.ts} +36 -16
  47. package/dist/tools/add-tasks.d.ts.map +1 -0
  48. package/dist/tools/{tasks-add-multiple.js → add-tasks.js} +39 -4
  49. package/dist/tools/complete-tasks.d.ts +40 -0
  50. package/dist/tools/complete-tasks.d.ts.map +1 -0
  51. package/dist/tools/complete-tasks.js +68 -0
  52. package/dist/tools/delete-object.d.ts +38 -0
  53. package/dist/tools/delete-object.d.ts.map +1 -0
  54. package/dist/tools/delete-object.js +69 -0
  55. package/dist/tools/find-completed-tasks.d.ts +74 -0
  56. package/dist/tools/find-completed-tasks.d.ts.map +1 -0
  57. package/dist/tools/find-completed-tasks.js +112 -0
  58. package/dist/tools/find-projects.d.ts +53 -0
  59. package/dist/tools/find-projects.d.ts.map +1 -0
  60. package/dist/tools/find-projects.js +101 -0
  61. package/dist/tools/find-sections.d.ts +42 -0
  62. package/dist/tools/find-sections.d.ts.map +1 -0
  63. package/dist/tools/find-sections.js +96 -0
  64. package/dist/tools/find-tasks-by-date.d.ts +59 -0
  65. package/dist/tools/find-tasks-by-date.d.ts.map +1 -0
  66. package/dist/tools/find-tasks-by-date.js +121 -0
  67. package/dist/tools/find-tasks.d.ts +65 -0
  68. package/dist/tools/find-tasks.d.ts.map +1 -0
  69. package/dist/tools/find-tasks.js +182 -0
  70. package/dist/tools/get-overview.d.ts +67 -0
  71. package/dist/tools/get-overview.d.ts.map +1 -0
  72. package/dist/tools/{overview.js → get-overview.js} +66 -19
  73. package/dist/tools/manage-projects.d.ts +35 -0
  74. package/dist/tools/manage-projects.d.ts.map +1 -0
  75. package/dist/tools/manage-projects.js +63 -0
  76. package/dist/tools/manage-sections.d.ts +38 -0
  77. package/dist/tools/manage-sections.d.ts.map +1 -0
  78. package/dist/tools/manage-sections.js +78 -0
  79. package/dist/tools/update-tasks.d.ts +94 -0
  80. package/dist/tools/update-tasks.d.ts.map +1 -0
  81. package/dist/tools/update-tasks.js +120 -0
  82. package/dist/utils/constants.d.ts +35 -0
  83. package/dist/utils/constants.d.ts.map +1 -0
  84. package/dist/utils/constants.js +37 -0
  85. package/dist/utils/response-builders.d.ts +88 -0
  86. package/dist/utils/response-builders.d.ts.map +1 -0
  87. package/dist/utils/response-builders.js +202 -0
  88. package/dist/{tools → utils}/test-helpers.d.ts +16 -0
  89. package/dist/utils/test-helpers.d.ts.map +1 -0
  90. package/dist/{tools → utils}/test-helpers.js +51 -0
  91. package/dist/utils/tool-names.d.ts +23 -0
  92. package/dist/utils/tool-names.d.ts.map +1 -0
  93. package/dist/utils/tool-names.js +25 -0
  94. package/package.json +1 -1
  95. package/dist/tools/__tests__/delete-one.test.d.ts +0 -2
  96. package/dist/tools/__tests__/delete-one.test.d.ts.map +0 -1
  97. package/dist/tools/__tests__/overview.test.d.ts +0 -2
  98. package/dist/tools/__tests__/overview.test.d.ts.map +0 -1
  99. package/dist/tools/__tests__/projects-list.test.d.ts +0 -2
  100. package/dist/tools/__tests__/projects-list.test.d.ts.map +0 -1
  101. package/dist/tools/__tests__/projects-manage.test.d.ts +0 -2
  102. package/dist/tools/__tests__/projects-manage.test.d.ts.map +0 -1
  103. package/dist/tools/__tests__/sections-manage.test.d.ts +0 -2
  104. package/dist/tools/__tests__/sections-manage.test.d.ts.map +0 -1
  105. package/dist/tools/__tests__/sections-manage.test.js +0 -138
  106. package/dist/tools/__tests__/sections-search.test.d.ts +0 -2
  107. package/dist/tools/__tests__/sections-search.test.d.ts.map +0 -1
  108. package/dist/tools/__tests__/tasks-add-multiple.test.d.ts +0 -2
  109. package/dist/tools/__tests__/tasks-add-multiple.test.d.ts.map +0 -1
  110. package/dist/tools/__tests__/tasks-complete-multiple.test.d.ts +0 -2
  111. package/dist/tools/__tests__/tasks-complete-multiple.test.d.ts.map +0 -1
  112. package/dist/tools/__tests__/tasks-complete-multiple.test.js +0 -146
  113. package/dist/tools/__tests__/tasks-list-by-date.test.d.ts +0 -2
  114. package/dist/tools/__tests__/tasks-list-by-date.test.d.ts.map +0 -1
  115. package/dist/tools/__tests__/tasks-list-completed.test.d.ts +0 -2
  116. package/dist/tools/__tests__/tasks-list-completed.test.d.ts.map +0 -1
  117. package/dist/tools/__tests__/tasks-list-for-container.test.d.ts +0 -2
  118. package/dist/tools/__tests__/tasks-list-for-container.test.d.ts.map +0 -1
  119. package/dist/tools/__tests__/tasks-list-for-container.test.js +0 -232
  120. package/dist/tools/__tests__/tasks-organize-multiple.test.d.ts +0 -2
  121. package/dist/tools/__tests__/tasks-organize-multiple.test.d.ts.map +0 -1
  122. package/dist/tools/__tests__/tasks-organize-multiple.test.js +0 -245
  123. package/dist/tools/__tests__/tasks-search.test.d.ts +0 -2
  124. package/dist/tools/__tests__/tasks-search.test.d.ts.map +0 -1
  125. package/dist/tools/__tests__/tasks-search.test.js +0 -106
  126. package/dist/tools/__tests__/tasks-update-one.test.d.ts +0 -2
  127. package/dist/tools/__tests__/tasks-update-one.test.d.ts.map +0 -1
  128. package/dist/tools/__tests__/tasks-update-one.test.js +0 -251
  129. package/dist/tools/delete-one.d.ts +0 -17
  130. package/dist/tools/delete-one.d.ts.map +0 -1
  131. package/dist/tools/delete-one.js +0 -25
  132. package/dist/tools/overview.d.ts +0 -14
  133. package/dist/tools/overview.d.ts.map +0 -1
  134. package/dist/tools/projects-list.d.ts +0 -29
  135. package/dist/tools/projects-list.d.ts.map +0 -1
  136. package/dist/tools/projects-list.js +0 -39
  137. package/dist/tools/projects-manage.d.ts +0 -24
  138. package/dist/tools/projects-manage.d.ts.map +0 -1
  139. package/dist/tools/projects-manage.js +0 -26
  140. package/dist/tools/sections-manage.d.ts +0 -23
  141. package/dist/tools/sections-manage.d.ts.map +0 -1
  142. package/dist/tools/sections-manage.js +0 -37
  143. package/dist/tools/sections-search.d.ts +0 -18
  144. package/dist/tools/sections-search.d.ts.map +0 -1
  145. package/dist/tools/sections-search.js +0 -27
  146. package/dist/tools/tasks-add-multiple.d.ts.map +0 -1
  147. package/dist/tools/tasks-complete-multiple.d.ts +0 -16
  148. package/dist/tools/tasks-complete-multiple.d.ts.map +0 -1
  149. package/dist/tools/tasks-complete-multiple.js +0 -23
  150. package/dist/tools/tasks-list-by-date.d.ts +0 -34
  151. package/dist/tools/tasks-list-by-date.d.ts.map +0 -1
  152. package/dist/tools/tasks-list-by-date.js +0 -53
  153. package/dist/tools/tasks-list-completed.d.ts +0 -44
  154. package/dist/tools/tasks-list-completed.d.ts.map +0 -1
  155. package/dist/tools/tasks-list-completed.js +0 -49
  156. package/dist/tools/tasks-list-for-container.d.ts +0 -34
  157. package/dist/tools/tasks-list-for-container.d.ts.map +0 -1
  158. package/dist/tools/tasks-list-for-container.js +0 -48
  159. package/dist/tools/tasks-organize-multiple.d.ts +0 -37
  160. package/dist/tools/tasks-organize-multiple.d.ts.map +0 -1
  161. package/dist/tools/tasks-organize-multiple.js +0 -34
  162. package/dist/tools/tasks-search.d.ts +0 -32
  163. package/dist/tools/tasks-search.d.ts.map +0 -1
  164. package/dist/tools/tasks-search.js +0 -30
  165. package/dist/tools/tasks-update-one.d.ts +0 -29
  166. package/dist/tools/tasks-update-one.d.ts.map +0 -1
  167. package/dist/tools/tasks-update-one.js +0 -63
  168. package/dist/tools/test-helpers.d.ts.map +0 -1
@@ -0,0 +1,334 @@
1
+ import { jest } from '@jest/globals';
2
+ import { getTasksByFilter } from '../../tool-helpers.js';
3
+ import { TEST_ERRORS, TEST_IDS, TODAY, createMappedTask, createMockApiResponse, createMockTask, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
4
+ import { ToolNames } from '../../utils/tool-names.js';
5
+ import { findTasks } from '../find-tasks.js';
6
+ jest.mock('../../tool-helpers', () => {
7
+ const actual = jest.requireActual('../../tool-helpers');
8
+ return {
9
+ getTasksByFilter: jest.fn(),
10
+ mapTask: actual.mapTask,
11
+ };
12
+ });
13
+ const { FIND_TASKS, UPDATE_TASKS, FIND_COMPLETED_TASKS } = ToolNames;
14
+ const mockGetTasksByFilter = getTasksByFilter;
15
+ // Mock the Todoist API
16
+ const mockTodoistApi = {
17
+ getTasks: jest.fn(),
18
+ };
19
+ describe(`${FIND_TASKS} tool`, () => {
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ });
23
+ describe('searching tasks', () => {
24
+ it('should search tasks and return results', async () => {
25
+ const mockTasks = [
26
+ createMappedTask({
27
+ id: TEST_IDS.TASK_1,
28
+ content: 'Task containing search term',
29
+ description: 'Description with more details',
30
+ labels: ['work'],
31
+ }),
32
+ createMappedTask({
33
+ id: TEST_IDS.TASK_2,
34
+ content: 'Another matching task',
35
+ priority: 2,
36
+ sectionId: TEST_IDS.SECTION_1,
37
+ }),
38
+ ];
39
+ const mockResponse = { tasks: mockTasks, nextCursor: 'cursor-for-next-page' };
40
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
41
+ const result = await findTasks.execute({
42
+ searchText: 'important meeting',
43
+ limit: 10,
44
+ }, mockTodoistApi);
45
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
46
+ client: mockTodoistApi,
47
+ query: 'search: important meeting',
48
+ cursor: undefined,
49
+ limit: 10,
50
+ });
51
+ // Verify result is a concise summary
52
+ expect(extractTextContent(result)).toMatchSnapshot();
53
+ // Verify structured content
54
+ const structuredContent = extractStructuredContent(result);
55
+ expect(structuredContent).toEqual(expect.objectContaining({
56
+ tasks: expect.any(Array),
57
+ totalCount: 2,
58
+ hasMore: true,
59
+ nextCursor: 'cursor-for-next-page',
60
+ appliedFilters: {
61
+ searchText: 'important meeting',
62
+ limit: 10,
63
+ cursor: undefined,
64
+ },
65
+ }));
66
+ expect(structuredContent).toEqual(expect.objectContaining({
67
+ tasks: expect.any(Array),
68
+ }));
69
+ });
70
+ it.each([
71
+ {
72
+ name: 'custom limit',
73
+ params: { searchText: 'project update', limit: 5 },
74
+ expectedQuery: 'search: project update',
75
+ expectedLimit: 5,
76
+ expectedCursor: undefined,
77
+ },
78
+ {
79
+ name: 'pagination cursor',
80
+ params: { searchText: 'follow up', limit: 20, cursor: 'cursor-from-first-page' },
81
+ expectedQuery: 'search: follow up',
82
+ expectedLimit: 20,
83
+ expectedCursor: 'cursor-from-first-page',
84
+ },
85
+ ])('should handle $name', async ({ params, expectedQuery, expectedLimit, expectedCursor }) => {
86
+ const mockTask = createMappedTask({ content: 'Test result' });
87
+ const mockResponse = { tasks: [mockTask], nextCursor: null };
88
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
89
+ const result = await findTasks.execute(params, mockTodoistApi);
90
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
91
+ client: mockTodoistApi,
92
+ query: expectedQuery,
93
+ cursor: expectedCursor,
94
+ limit: expectedLimit,
95
+ });
96
+ // Verify result is a concise summary
97
+ expect(extractTextContent(result)).toMatchSnapshot();
98
+ // Verify structured content
99
+ const structuredContent = extractStructuredContent(result);
100
+ expect(structuredContent).toEqual(expect.objectContaining({
101
+ tasks: expect.any(Array),
102
+ totalCount: 1,
103
+ hasMore: false,
104
+ nextCursor: null,
105
+ appliedFilters: expect.objectContaining({
106
+ searchText: params.searchText,
107
+ limit: expectedLimit,
108
+ }),
109
+ }));
110
+ expect(structuredContent).toEqual(expect.objectContaining({
111
+ tasks: expect.any(Array),
112
+ }));
113
+ });
114
+ it.each([
115
+ { searchText: '@work #urgent "exact phrase"', description: 'special characters' },
116
+ { searchText: 'nonexistent keyword', description: 'empty results' },
117
+ ])('should handle search with $description', async ({ searchText }) => {
118
+ const mockResponse = { tasks: [], nextCursor: null };
119
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
120
+ const result = await findTasks.execute({ searchText, limit: 10 }, mockTodoistApi);
121
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
122
+ client: mockTodoistApi,
123
+ query: `search: ${searchText}`,
124
+ cursor: undefined,
125
+ limit: 10,
126
+ });
127
+ // Verify result is a concise summary
128
+ expect(extractTextContent(result)).toMatchSnapshot();
129
+ // Verify structured content for empty results
130
+ const structuredContent = extractStructuredContent(result);
131
+ expect(structuredContent).toEqual(expect.objectContaining({
132
+ tasks: [],
133
+ totalCount: 0,
134
+ hasMore: false,
135
+ nextCursor: null,
136
+ appliedFilters: expect.objectContaining({
137
+ searchText: searchText,
138
+ }),
139
+ }));
140
+ });
141
+ });
142
+ describe('validation', () => {
143
+ 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');
145
+ });
146
+ });
147
+ describe('container filtering', () => {
148
+ it.each([
149
+ {
150
+ name: 'project',
151
+ params: { projectId: TEST_IDS.PROJECT_TEST, limit: 10 },
152
+ expectedApiParam: { projectId: TEST_IDS.PROJECT_TEST },
153
+ tasks: [createMockTask({ content: 'Project task' })],
154
+ },
155
+ {
156
+ name: 'section',
157
+ params: { sectionId: TEST_IDS.SECTION_1, limit: 10 },
158
+ expectedApiParam: { sectionId: TEST_IDS.SECTION_1 },
159
+ tasks: [createMockTask({ content: 'Section task' })],
160
+ },
161
+ {
162
+ name: 'parent task',
163
+ params: { parentId: TEST_IDS.TASK_1, limit: 10 },
164
+ expectedApiParam: { parentId: TEST_IDS.TASK_1 },
165
+ tasks: [createMockTask({ content: 'Subtask' })],
166
+ },
167
+ ])('should find tasks in $name', async ({ params, expectedApiParam, tasks }) => {
168
+ mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(tasks));
169
+ const result = await findTasks.execute(params, mockTodoistApi);
170
+ expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
171
+ limit: 10,
172
+ cursor: null,
173
+ ...expectedApiParam,
174
+ });
175
+ expect(extractTextContent(result)).toMatchSnapshot();
176
+ const structuredContent = extractStructuredContent(result);
177
+ expect(structuredContent).toEqual(expect.objectContaining({
178
+ tasks: expect.any(Array),
179
+ totalCount: tasks.length,
180
+ hasMore: false,
181
+ appliedFilters: params,
182
+ }));
183
+ });
184
+ it('should handle combined search text and container filtering', async () => {
185
+ const tasks = [
186
+ createMockTask({
187
+ id: '8485093749',
188
+ content: 'relevant task',
189
+ description: 'contains search term',
190
+ }),
191
+ createMockTask({
192
+ id: '8485093750',
193
+ content: 'other task',
194
+ description: 'different content',
195
+ }),
196
+ ];
197
+ mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(tasks));
198
+ const result = await findTasks.execute({
199
+ projectId: TEST_IDS.PROJECT_TEST,
200
+ searchText: 'relevant',
201
+ limit: 10,
202
+ }, mockTodoistApi);
203
+ expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
204
+ limit: 10,
205
+ cursor: null,
206
+ projectId: TEST_IDS.PROJECT_TEST,
207
+ });
208
+ const structuredContent = extractStructuredContent(result);
209
+ expect(structuredContent.tasks).toHaveLength(1);
210
+ expect(structuredContent.tasks).toEqual([
211
+ expect.objectContaining({ content: 'relevant task' }),
212
+ ]);
213
+ });
214
+ it('should handle empty containers', async () => {
215
+ mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse([]));
216
+ const result = await findTasks.execute({
217
+ sectionId: 'empty-section',
218
+ limit: 10,
219
+ }, mockTodoistApi);
220
+ const textContent = extractTextContent(result);
221
+ expect(textContent).toContain('Section is empty');
222
+ expect(textContent).toContain('Tasks may be in other sections of the project');
223
+ });
224
+ it('should handle pagination with containers', async () => {
225
+ mockTodoistApi.getTasks.mockResolvedValue({
226
+ results: [],
227
+ nextCursor: 'next-cursor',
228
+ });
229
+ const result = await findTasks.execute({
230
+ projectId: TEST_IDS.PROJECT_TEST,
231
+ limit: 25,
232
+ cursor: 'current-cursor',
233
+ }, mockTodoistApi);
234
+ expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
235
+ limit: 25,
236
+ cursor: 'current-cursor',
237
+ projectId: TEST_IDS.PROJECT_TEST,
238
+ });
239
+ const structuredContent = extractStructuredContent(result);
240
+ expect(structuredContent.hasMore).toBe(true);
241
+ expect(structuredContent.nextCursor).toBe('next-cursor');
242
+ });
243
+ });
244
+ describe('container error handling', () => {
245
+ it('should propagate API errors for container queries', async () => {
246
+ const apiError = new Error('API Error: Project not found');
247
+ mockTodoistApi.getTasks.mockRejectedValue(apiError);
248
+ await expect(findTasks.execute({ projectId: 'non-existent', limit: 10 }, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
249
+ });
250
+ });
251
+ describe('next steps logic', () => {
252
+ it('should suggest different actions when hasOverdue is true', async () => {
253
+ const mockTasks = [
254
+ createMappedTask({
255
+ id: TEST_IDS.TASK_1,
256
+ content: 'Overdue search result',
257
+ dueDate: '2025-08-10', // Past date
258
+ }),
259
+ ];
260
+ const mockResponse = { tasks: mockTasks, nextCursor: null };
261
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
262
+ const result = await findTasks.execute({ searchText: 'overdue tasks', limit: 10 }, mockTodoistApi);
263
+ const textContent = extractTextContent(result);
264
+ expect(textContent).toMatchSnapshot();
265
+ // Should prioritize overdue tasks in next steps
266
+ expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
267
+ });
268
+ it('should suggest today tasks when hasToday is true', async () => {
269
+ const mockTasks = [
270
+ createMappedTask({
271
+ id: TEST_IDS.TASK_1,
272
+ content: 'Task due today',
273
+ dueDate: TODAY,
274
+ }),
275
+ ];
276
+ const mockResponse = { tasks: mockTasks, nextCursor: null };
277
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
278
+ const result = await findTasks.execute({ searchText: 'today tasks', limit: 10 }, mockTodoistApi);
279
+ const textContent = extractTextContent(result);
280
+ expect(textContent).toMatchSnapshot();
281
+ // Should suggest today-focused actions
282
+ expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
283
+ });
284
+ it('should provide different next steps for regular tasks', async () => {
285
+ const mockTasks = [
286
+ createMappedTask({
287
+ id: TEST_IDS.TASK_1,
288
+ content: 'Regular future task',
289
+ dueDate: '2025-08-25', // Future date
290
+ }),
291
+ ];
292
+ const mockResponse = { tasks: mockTasks, nextCursor: null };
293
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
294
+ const result = await findTasks.execute({ searchText: 'future tasks', limit: 10 }, mockTodoistApi);
295
+ const textContent = extractTextContent(result);
296
+ expect(textContent).toMatchSnapshot();
297
+ expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
298
+ });
299
+ it('should provide helpful suggestions for empty search results', async () => {
300
+ const mockResponse = { tasks: [], nextCursor: null };
301
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
302
+ const result = await findTasks.execute({ searchText: 'nonexistent', limit: 10 }, mockTodoistApi);
303
+ const textContent = extractTextContent(result);
304
+ expect(textContent).toMatchSnapshot();
305
+ expect(textContent).toContain('Try broader search terms');
306
+ expect(textContent).toContain(`Check completed tasks with ${FIND_COMPLETED_TASKS}`);
307
+ expect(textContent).toContain('Verify spelling and try partial words');
308
+ });
309
+ });
310
+ describe('error handling', () => {
311
+ it.each([
312
+ {
313
+ error: 'At least one filter must be provided: searchText, projectId, sectionId, or parentId',
314
+ params: { limit: 10 },
315
+ expectValidation: true,
316
+ },
317
+ {
318
+ error: TEST_ERRORS.API_RATE_LIMIT,
319
+ params: { searchText: 'any search term', limit: 10 },
320
+ expectValidation: false,
321
+ },
322
+ {
323
+ error: TEST_ERRORS.INVALID_CURSOR,
324
+ params: { searchText: 'test', cursor: 'invalid-cursor-format', limit: 10 },
325
+ expectValidation: false,
326
+ },
327
+ ])('should propagate $error', async ({ error, params, expectValidation }) => {
328
+ if (!expectValidation) {
329
+ mockGetTasksByFilter.mockRejectedValue(new Error(error));
330
+ }
331
+ await expect(findTasks.execute(params, mockTodoistApi)).rejects.toThrow(error);
332
+ });
333
+ });
334
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=get-overview.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-overview.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/get-overview.test.ts"],"names":[],"mappings":""}
@@ -1,6 +1,7 @@
1
1
  import { jest } from '@jest/globals';
2
- import { overview } from '../overview.js';
3
- import { TEST_ERRORS, TEST_IDS, createMockProject, createMockSection, createMockTask, } from '../test-helpers.js';
2
+ import { TEST_ERRORS, TEST_IDS, createMockProject, createMockSection, createMockTask, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
3
+ import { ToolNames } from '../../utils/tool-names.js';
4
+ import { getOverview } from '../get-overview.js';
4
5
  // Mock the Todoist API
5
6
  const mockTodoistApi = {
6
7
  getProjects: jest.fn(),
@@ -8,7 +9,8 @@ const mockTodoistApi = {
8
9
  getSections: jest.fn(),
9
10
  getTasks: jest.fn(),
10
11
  };
11
- describe('overview tool', () => {
12
+ const { GET_OVERVIEW } = ToolNames;
13
+ describe(`${GET_OVERVIEW} tool`, () => {
12
14
  beforeEach(() => {
13
15
  jest.clearAllMocks();
14
16
  });
@@ -45,15 +47,42 @@ describe('overview tool', () => {
45
47
  }
46
48
  return Promise.resolve({ results: [], nextCursor: null });
47
49
  });
48
- const result = await overview.execute({}, mockTodoistApi);
50
+ const result = await getOverview.execute({}, mockTodoistApi);
49
51
  expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({});
50
52
  expect(mockTodoistApi.getSections).toHaveBeenCalledTimes(2); // Once for each project
51
- // Use snapshot testing for complex markdown output
52
- expect(result).toMatchSnapshot();
53
+ // Test text content with snapshot
54
+ expect(extractTextContent(result)).toMatchSnapshot();
55
+ // Test structured content sanity checks
56
+ const structuredContent = extractStructuredContent(result);
57
+ expect(structuredContent).toEqual(expect.objectContaining({
58
+ type: 'account_overview',
59
+ inbox: expect.objectContaining({
60
+ id: TEST_IDS.PROJECT_INBOX,
61
+ name: 'Inbox',
62
+ sections: expect.any(Array),
63
+ }),
64
+ projects: expect.any(Array),
65
+ totalProjects: 2,
66
+ totalSections: 1,
67
+ hasNestedProjects: false,
68
+ }));
69
+ expect(structuredContent.projects).toHaveLength(1); // Only non-inbox projects
53
70
  });
54
71
  it('should handle empty projects list', async () => {
55
72
  mockTodoistApi.getProjects.mockResolvedValue({ results: [], nextCursor: null });
56
- expect(await overview.execute({}, mockTodoistApi)).toMatchSnapshot();
73
+ const result = await getOverview.execute({}, mockTodoistApi);
74
+ // Test text content with snapshot
75
+ expect(extractTextContent(result)).toMatchSnapshot();
76
+ // Test structured content sanity checks
77
+ const structuredContent = extractStructuredContent(result);
78
+ expect(structuredContent).toEqual(expect.objectContaining({
79
+ type: 'account_overview',
80
+ inbox: null,
81
+ projects: [],
82
+ totalProjects: 0,
83
+ totalSections: 0,
84
+ hasNestedProjects: false,
85
+ }));
57
86
  });
58
87
  });
59
88
  describe('project overview (with projectId)', () => {
@@ -114,7 +143,7 @@ describe('overview tool', () => {
114
143
  results: mockTasks,
115
144
  nextCursor: null,
116
145
  });
117
- const result = await overview.execute({ projectId: TEST_IDS.PROJECT_TEST }, mockTodoistApi);
146
+ const result = await getOverview.execute({ projectId: TEST_IDS.PROJECT_TEST }, mockTodoistApi);
118
147
  expect(mockTodoistApi.getProject).toHaveBeenCalledWith(TEST_IDS.PROJECT_TEST);
119
148
  expect(mockTodoistApi.getSections).toHaveBeenCalledWith({
120
149
  projectId: TEST_IDS.PROJECT_TEST,
@@ -124,8 +153,26 @@ describe('overview tool', () => {
124
153
  limit: 50,
125
154
  cursor: undefined,
126
155
  });
127
- // Use snapshot testing for complex markdown output
128
- expect(result).toMatchSnapshot();
156
+ // Test text content with snapshot
157
+ expect(extractTextContent(result)).toMatchSnapshot();
158
+ // Test structured content sanity checks
159
+ const structuredContent = extractStructuredContent(result);
160
+ expect(structuredContent).toEqual(expect.objectContaining({
161
+ type: 'project_overview',
162
+ project: expect.objectContaining({
163
+ id: TEST_IDS.PROJECT_TEST,
164
+ name: 'test-abc123def456-project',
165
+ }),
166
+ sections: expect.any(Array),
167
+ tasks: expect.any(Array),
168
+ stats: expect.objectContaining({
169
+ totalTasks: 3,
170
+ totalSections: 2,
171
+ tasksWithoutSection: 1,
172
+ }),
173
+ }));
174
+ expect(structuredContent.sections).toHaveLength(2);
175
+ expect(structuredContent.tasks).toHaveLength(3);
129
176
  });
130
177
  it('should handle project with no tasks', async () => {
131
178
  const mockProject = createMockProject({
@@ -136,8 +183,25 @@ describe('overview tool', () => {
136
183
  mockTodoistApi.getProject.mockResolvedValue(mockProject);
137
184
  mockTodoistApi.getSections.mockResolvedValue({ results: [], nextCursor: null });
138
185
  mockTodoistApi.getTasks.mockResolvedValue({ results: [], nextCursor: null });
139
- const result = await overview.execute({ projectId: 'empty-project-id' }, mockTodoistApi);
140
- expect(result).toMatchSnapshot();
186
+ const result = await getOverview.execute({ projectId: 'empty-project-id' }, mockTodoistApi);
187
+ // Test text content with snapshot
188
+ expect(extractTextContent(result)).toMatchSnapshot();
189
+ // Test structured content sanity checks
190
+ const structuredContent = extractStructuredContent(result);
191
+ expect(structuredContent).toEqual(expect.objectContaining({
192
+ type: 'project_overview',
193
+ project: expect.objectContaining({
194
+ id: 'empty-project-id',
195
+ name: 'Empty Project',
196
+ }),
197
+ sections: [],
198
+ tasks: [],
199
+ stats: expect.objectContaining({
200
+ totalTasks: 0,
201
+ totalSections: 0,
202
+ tasksWithoutSection: 0,
203
+ }),
204
+ }));
141
205
  });
142
206
  });
143
207
  describe('error handling', () => {
@@ -157,7 +221,7 @@ describe('overview tool', () => {
157
221
  ])('should propagate API errors for $scenario', async ({ error, params, mockMethod }) => {
158
222
  const apiError = new Error(error);
159
223
  mockTodoistApi[mockMethod].mockRejectedValue(apiError);
160
- await expect(overview.execute(params, mockTodoistApi)).rejects.toThrow(error);
224
+ await expect(getOverview.execute(params, mockTodoistApi)).rejects.toThrow(error);
161
225
  });
162
226
  });
163
227
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=manage-projects.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manage-projects.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/manage-projects.test.ts"],"names":[],"mappings":""}
@@ -1,12 +1,14 @@
1
1
  import { jest } from '@jest/globals';
2
- import { projectsManage } from '../projects-manage.js';
3
- import { TEST_IDS, createMockProject } from '../test-helpers.js';
2
+ import { TEST_IDS, createMockProject, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
3
+ import { ToolNames } from '../../utils/tool-names.js';
4
+ import { manageProjects } from '../manage-projects.js';
4
5
  // Mock the Todoist API
5
6
  const mockTodoistApi = {
6
7
  addProject: jest.fn(),
7
8
  updateProject: jest.fn(),
8
9
  };
9
- describe('projects-manage tool', () => {
10
+ const { ADD_TASKS, FIND_PROJECTS, MANAGE_PROJECTS, MANAGE_SECTIONS } = ToolNames;
11
+ describe(`${MANAGE_PROJECTS} tool`, () => {
10
12
  beforeEach(() => {
11
13
  jest.clearAllMocks();
12
14
  });
@@ -19,22 +21,25 @@ describe('projects-manage tool', () => {
19
21
  createdAt: '2024-01-01T00:00:00Z',
20
22
  });
21
23
  mockTodoistApi.addProject.mockResolvedValue(mockApiResponse);
22
- const result = await projectsManage.execute({ name: 'test-abc123def456-project' }, mockTodoistApi);
24
+ const result = await manageProjects.execute({ name: 'test-abc123def456-project' }, mockTodoistApi);
23
25
  // Verify API was called correctly
24
26
  expect(mockTodoistApi.addProject).toHaveBeenCalledWith({
25
27
  name: 'test-abc123def456-project',
26
28
  });
27
- // Verify result is properly mapped
28
- expect(result).toEqual({
29
- id: TEST_IDS.PROJECT_TEST,
30
- name: 'test-abc123def456-project',
31
- color: 'charcoal',
32
- isFavorite: false,
33
- isShared: false,
34
- parentId: null,
35
- inboxProject: false,
36
- viewStyle: 'list',
37
- });
29
+ const textContent = extractTextContent(result);
30
+ expect(textContent).toMatchSnapshot();
31
+ expect(textContent).toContain('Created project: test-abc123def456-project');
32
+ expect(textContent).toContain(`id=${TEST_IDS.PROJECT_TEST}`);
33
+ expect(textContent).toContain(`Use ${ADD_TASKS} to add your first tasks`);
34
+ // Verify structured content
35
+ const structuredContent = extractStructuredContent(result);
36
+ expect(structuredContent).toEqual(expect.objectContaining({
37
+ project: expect.objectContaining({
38
+ id: TEST_IDS.PROJECT_TEST,
39
+ name: 'test-abc123def456-project',
40
+ }),
41
+ operation: 'created',
42
+ }));
38
43
  });
39
44
  it('should handle different project properties from API', async () => {
40
45
  const mockApiResponse = createMockProject({
@@ -50,18 +55,13 @@ describe('projects-manage tool', () => {
50
55
  createdAt: '2024-01-01T00:00:00Z',
51
56
  });
52
57
  mockTodoistApi.addProject.mockResolvedValue(mockApiResponse);
53
- const result = await projectsManage.execute({ name: 'My Blue Project' }, mockTodoistApi);
58
+ const result = await manageProjects.execute({ name: 'My Blue Project' }, mockTodoistApi);
54
59
  expect(mockTodoistApi.addProject).toHaveBeenCalledWith({ name: 'My Blue Project' });
55
- expect(result).toEqual({
56
- id: 'project-456',
57
- name: 'My Blue Project',
58
- color: 'blue',
59
- isFavorite: true,
60
- isShared: true,
61
- parentId: 'parent-123',
62
- inboxProject: false,
63
- viewStyle: 'board',
64
- });
60
+ const textContent = extractTextContent(result);
61
+ expect(textContent).toMatchSnapshot();
62
+ expect(textContent).toContain('Created project: My Blue Project');
63
+ expect(textContent).toContain('id=project-456');
64
+ expect(textContent).toContain(`Use ${MANAGE_SECTIONS} to organize this project`);
65
65
  });
66
66
  });
67
67
  describe('updating an existing project', () => {
@@ -88,19 +88,22 @@ describe('projects-manage tool', () => {
88
88
  defaultOrder: 0,
89
89
  };
90
90
  mockTodoistApi.updateProject.mockResolvedValue(mockApiResponse);
91
- const result = await projectsManage.execute({ id: 'existing-project-123', name: 'Updated Project Name' }, mockTodoistApi);
91
+ const result = await manageProjects.execute({ id: 'existing-project-123', name: 'Updated Project Name' }, mockTodoistApi);
92
92
  expect(mockTodoistApi.updateProject).toHaveBeenCalledWith('existing-project-123', {
93
93
  name: 'Updated Project Name',
94
94
  });
95
- // Update returns raw project (not mapped) - this is the actual behavior
96
- expect(result).toEqual(mockApiResponse);
95
+ const textContent = extractTextContent(result);
96
+ expect(textContent).toMatchSnapshot();
97
+ expect(textContent).toContain('Updated project: Updated Project Name');
98
+ expect(textContent).toContain('id=existing-project-123');
99
+ expect(textContent).toContain(`Use ${FIND_PROJECTS} to see all projects`);
97
100
  });
98
101
  });
99
102
  describe('error handling', () => {
100
103
  it('should propagate API errors', async () => {
101
104
  const apiError = new Error('API Error: Project name is required');
102
105
  mockTodoistApi.addProject.mockRejectedValue(apiError);
103
- await expect(projectsManage.execute({ name: '' }, mockTodoistApi)).rejects.toThrow('API Error: Project name is required');
106
+ await expect(manageProjects.execute({ name: '' }, mockTodoistApi)).rejects.toThrow('API Error: Project name is required');
104
107
  });
105
108
  });
106
109
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=manage-sections.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manage-sections.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/manage-sections.test.ts"],"names":[],"mappings":""}