@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,206 @@
1
+ import { jest } from '@jest/globals';
2
+ import { extractTextContent } from '../../utils/test-helpers.js';
3
+ import { ToolNames } from '../../utils/tool-names.js';
4
+ import { completeTasks } from '../complete-tasks.js';
5
+ // Mock the Todoist API
6
+ const mockTodoistApi = {
7
+ closeTask: jest.fn(),
8
+ };
9
+ const { COMPLETE_TASKS, GET_OVERVIEW } = ToolNames;
10
+ describe(`${COMPLETE_TASKS} tool`, () => {
11
+ beforeEach(() => {
12
+ jest.clearAllMocks();
13
+ });
14
+ describe('completing multiple tasks', () => {
15
+ it('should complete all tasks successfully', async () => {
16
+ mockTodoistApi.closeTask.mockResolvedValue(true);
17
+ const result = await completeTasks.execute({ ids: ['task-1', 'task-2', 'task-3'] }, mockTodoistApi);
18
+ // Verify API was called for each task
19
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(3);
20
+ expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(1, 'task-1');
21
+ expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(2, 'task-2');
22
+ expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(3, 'task-3');
23
+ // Verify result is a concise summary
24
+ expect(extractTextContent(result)).toMatchSnapshot();
25
+ // Verify structured content
26
+ const { structuredContent } = result;
27
+ expect(structuredContent).toEqual(expect.objectContaining({
28
+ completed: ['task-1', 'task-2', 'task-3'],
29
+ failures: [],
30
+ totalRequested: 3,
31
+ successCount: 3,
32
+ failureCount: 0,
33
+ }));
34
+ });
35
+ it('should complete single task', async () => {
36
+ mockTodoistApi.closeTask.mockResolvedValue(true);
37
+ const result = await completeTasks.execute({ ids: ['8485093748'] }, mockTodoistApi);
38
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(1);
39
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledWith('8485093748');
40
+ expect(extractTextContent(result)).toMatchSnapshot();
41
+ // Verify structured content
42
+ const { structuredContent } = result;
43
+ expect(structuredContent).toEqual(expect.objectContaining({
44
+ completed: ['8485093748'],
45
+ failures: [],
46
+ totalRequested: 1,
47
+ successCount: 1,
48
+ failureCount: 0,
49
+ }));
50
+ });
51
+ it('should handle partial failures gracefully', async () => {
52
+ // Mock first and third tasks to succeed, second to fail
53
+ mockTodoistApi.closeTask
54
+ .mockResolvedValueOnce(true) // task-1 succeeds
55
+ .mockRejectedValueOnce(new Error('Task not found')) // task-2 fails
56
+ .mockResolvedValueOnce(true); // task-3 succeeds
57
+ const result = await completeTasks.execute({ ids: ['task-1', 'task-2', 'task-3'] }, mockTodoistApi);
58
+ // Verify API was called for all tasks despite failure
59
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(3);
60
+ expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(1, 'task-1');
61
+ expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(2, 'task-2');
62
+ expect(mockTodoistApi.closeTask).toHaveBeenNthCalledWith(3, 'task-3');
63
+ // Verify only successful completions are reported
64
+ expect(extractTextContent(result)).toMatchSnapshot();
65
+ // Verify structured content with partial failures
66
+ const { structuredContent } = result;
67
+ expect(structuredContent).toEqual(expect.objectContaining({
68
+ completed: ['task-1', 'task-3'],
69
+ failures: [
70
+ expect.objectContaining({
71
+ item: 'task-2',
72
+ error: 'Task not found',
73
+ }),
74
+ ],
75
+ totalRequested: 3,
76
+ successCount: 2,
77
+ failureCount: 1,
78
+ }));
79
+ });
80
+ it('should handle all tasks failing', async () => {
81
+ const apiError = new Error('API Error: Network timeout');
82
+ mockTodoistApi.closeTask.mockRejectedValue(apiError);
83
+ const result = await completeTasks.execute({ ids: ['task-1', 'task-2'] }, mockTodoistApi);
84
+ // Verify API was attempted for all tasks
85
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(2);
86
+ // Verify no tasks were completed but still returns success
87
+ expect(extractTextContent(result)).toMatchSnapshot();
88
+ });
89
+ it('should continue processing remaining tasks after failures', async () => {
90
+ // Mock various failure scenarios
91
+ mockTodoistApi.closeTask
92
+ .mockRejectedValueOnce(new Error('Task already completed'))
93
+ .mockRejectedValueOnce(new Error('Task not found'))
94
+ .mockResolvedValueOnce(true) // task-3 succeeds
95
+ .mockRejectedValueOnce(new Error('Permission denied'))
96
+ .mockResolvedValueOnce(true); // task-5 succeeds
97
+ const result = await completeTasks.execute({ ids: ['task-1', 'task-2', 'task-3', 'task-4', 'task-5'] }, mockTodoistApi);
98
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(5);
99
+ // Only tasks 3 and 5 should be in completed list
100
+ expect(extractTextContent(result)).toMatchSnapshot();
101
+ });
102
+ it('should handle different types of API errors', async () => {
103
+ mockTodoistApi.closeTask
104
+ .mockRejectedValueOnce(new Error('Task not found'))
105
+ .mockRejectedValueOnce(new Error('Task already completed'))
106
+ .mockRejectedValueOnce(new Error('Permission denied'))
107
+ .mockRejectedValueOnce(new Error('Rate limit exceeded'));
108
+ const result = await completeTasks.execute({ ids: ['not-found', 'already-done', 'no-permission', 'rate-limited'] }, mockTodoistApi);
109
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(4);
110
+ // All should fail, but the tool should handle it gracefully
111
+ expect(extractTextContent(result)).toMatchSnapshot();
112
+ });
113
+ });
114
+ describe('mixed success and failure scenarios', () => {
115
+ it('should handle realistic mixed scenario', async () => {
116
+ // Simulate a realistic scenario with some tasks completing and others failing
117
+ mockTodoistApi.closeTask
118
+ .mockResolvedValueOnce(true) // regular task completion
119
+ .mockResolvedValueOnce(true) // another successful completion
120
+ .mockRejectedValueOnce(new Error('Task already completed')) // duplicate completion
121
+ .mockResolvedValueOnce(true) // successful completion
122
+ .mockRejectedValueOnce(new Error('Task not found')); // deleted task
123
+ const result = await completeTasks.execute({
124
+ ids: [
125
+ '8485093748', // regular task
126
+ '8485093749', // regular task
127
+ '8485093750', // already completed
128
+ '8485093751', // regular task
129
+ '8485093752', // deleted task
130
+ ],
131
+ }, mockTodoistApi);
132
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(5);
133
+ expect(extractTextContent(result)).toMatchSnapshot();
134
+ });
135
+ });
136
+ describe('next steps logic validation', () => {
137
+ it('should suggest overdue tasks when all tasks complete successfully', async () => {
138
+ mockTodoistApi.closeTask.mockResolvedValue(true);
139
+ const result = await completeTasks.execute({ ids: ['task-1', 'task-2'] }, mockTodoistApi);
140
+ const textContent = extractTextContent(result);
141
+ expect(textContent).toMatchSnapshot();
142
+ expect(textContent).toContain("Use find-tasks-by-date('overdue')");
143
+ });
144
+ it('should suggest reviewing failures when mixed results', async () => {
145
+ mockTodoistApi.closeTask
146
+ .mockResolvedValueOnce(true)
147
+ .mockRejectedValueOnce(new Error('Task not found'));
148
+ const result = await completeTasks.execute({ ids: ['task-1', 'task-2'] }, mockTodoistApi);
149
+ const textContent = extractTextContent(result);
150
+ expect(textContent).toMatchSnapshot();
151
+ expect(textContent).toContain('Review failed completions and retry if needed');
152
+ });
153
+ it('should suggest checking IDs when all tasks fail', async () => {
154
+ mockTodoistApi.closeTask.mockRejectedValue(new Error('Task not found'));
155
+ const result = await completeTasks.execute({ ids: ['bad-id-1', 'bad-id-2'] }, mockTodoistApi);
156
+ const textContent = extractTextContent(result);
157
+ expect(textContent).toMatchSnapshot();
158
+ expect(textContent).toContain('Check task IDs and permissions, then retry');
159
+ expect(textContent).not.toContain(`Use ${GET_OVERVIEW} tool`); // Should only show retry message
160
+ });
161
+ });
162
+ describe('error message truncation', () => {
163
+ it('should truncate failure messages after 3 errors', async () => {
164
+ mockTodoistApi.closeTask
165
+ .mockRejectedValueOnce(new Error('Error 1'))
166
+ .mockRejectedValueOnce(new Error('Error 2'))
167
+ .mockRejectedValueOnce(new Error('Error 3'))
168
+ .mockRejectedValueOnce(new Error('Error 4'))
169
+ .mockRejectedValueOnce(new Error('Error 5'));
170
+ const result = await completeTasks.execute({ ids: ['task-1', 'task-2', 'task-3', 'task-4', 'task-5'] }, mockTodoistApi);
171
+ const textContent = extractTextContent(result);
172
+ expect(textContent).toMatchSnapshot();
173
+ expect(textContent).toContain('+2 more'); // 5 total failures, showing first 3, so +2 more
174
+ expect(textContent).not.toContain('Error 4'); // Should not show 4th error
175
+ expect(textContent).not.toContain('Error 5'); // Should not show 5th error
176
+ });
177
+ it('should not show truncation message for exactly 3 errors', async () => {
178
+ mockTodoistApi.closeTask
179
+ .mockRejectedValueOnce(new Error('Error 1'))
180
+ .mockRejectedValueOnce(new Error('Error 2'))
181
+ .mockRejectedValueOnce(new Error('Error 3'));
182
+ const result = await completeTasks.execute({ ids: ['task-1', 'task-2', 'task-3'] }, mockTodoistApi);
183
+ const textContent = extractTextContent(result);
184
+ expect(textContent).toMatchSnapshot();
185
+ expect(textContent).not.toContain('more'); // Should not show truncation
186
+ });
187
+ });
188
+ describe('edge cases', () => {
189
+ it('should handle empty task completion (minimum one task required by schema)', async () => {
190
+ // Note: This test documents that the schema requires at least one task,
191
+ // so this scenario shouldn't occur in practice due to validation
192
+ mockTodoistApi.closeTask.mockResolvedValue(true);
193
+ const result = await completeTasks.execute({ ids: ['single-task'] }, mockTodoistApi);
194
+ expect(extractTextContent(result)).toMatchSnapshot();
195
+ });
196
+ it('should handle tasks with special ID formats', async () => {
197
+ mockTodoistApi.closeTask.mockResolvedValue(true);
198
+ const result = await completeTasks.execute({ ids: ['proj_123_task_456', 'task-with-dashes', '1234567890'] }, mockTodoistApi);
199
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledTimes(3);
200
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledWith('proj_123_task_456');
201
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledWith('task-with-dashes');
202
+ expect(mockTodoistApi.closeTask).toHaveBeenCalledWith('1234567890');
203
+ expect(extractTextContent(result)).toMatchSnapshot();
204
+ });
205
+ });
206
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=delete-object.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete-object.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/delete-object.test.ts"],"names":[],"mappings":""}
@@ -1,85 +1,105 @@
1
1
  import { jest } from '@jest/globals';
2
- import { deleteOne } from '../delete-one.js';
2
+ import { extractTextContent } from '../../utils/test-helpers.js';
3
+ import { ToolNames } from '../../utils/tool-names.js';
4
+ import { deleteObject } from '../delete-object.js';
3
5
  // Mock the Todoist API
4
6
  const mockTodoistApi = {
5
7
  deleteProject: jest.fn(),
6
8
  deleteSection: jest.fn(),
7
9
  deleteTask: jest.fn(),
8
10
  };
9
- describe('delete-one tool', () => {
11
+ const { FIND_PROJECTS, FIND_TASKS_BY_DATE, DELETE_OBJECT } = ToolNames;
12
+ describe(`${DELETE_OBJECT} tool`, () => {
10
13
  beforeEach(() => {
11
14
  jest.clearAllMocks();
12
15
  });
13
16
  describe('deleting projects', () => {
14
17
  it('should delete a project by ID', async () => {
15
18
  mockTodoistApi.deleteProject.mockResolvedValue(true);
16
- const result = await deleteOne.execute({ type: 'project', id: '6cfCcrrCFg2xP94Q' }, mockTodoistApi);
17
- // Verify API was called correctly
19
+ const result = await deleteObject.execute({ type: 'project', id: '6cfCcrrCFg2xP94Q' }, mockTodoistApi);
18
20
  expect(mockTodoistApi.deleteProject).toHaveBeenCalledWith('6cfCcrrCFg2xP94Q');
19
21
  expect(mockTodoistApi.deleteSection).not.toHaveBeenCalled();
20
22
  expect(mockTodoistApi.deleteTask).not.toHaveBeenCalled();
21
- // Verify success response
22
- expect(result).toEqual({ success: true });
23
+ const textContent = extractTextContent(result);
24
+ expect(textContent).toMatchSnapshot();
25
+ expect(textContent).toContain('Deleted project: id=6cfCcrrCFg2xP94Q');
26
+ expect(textContent).toContain(`Use ${FIND_PROJECTS} to see remaining projects`);
27
+ expect(result.structuredContent).toEqual({
28
+ deletedEntity: {
29
+ type: 'project',
30
+ id: '6cfCcrrCFg2xP94Q',
31
+ },
32
+ success: true,
33
+ });
23
34
  });
24
35
  it('should propagate project deletion errors', async () => {
25
36
  const apiError = new Error('API Error: Cannot delete project with tasks');
26
37
  mockTodoistApi.deleteProject.mockRejectedValue(apiError);
27
- await expect(deleteOne.execute({ type: 'project', id: 'project-with-tasks' }, mockTodoistApi)).rejects.toThrow('API Error: Cannot delete project with tasks');
38
+ await expect(deleteObject.execute({ type: 'project', id: 'project-with-tasks' }, mockTodoistApi)).rejects.toThrow('API Error: Cannot delete project with tasks');
28
39
  });
29
40
  });
30
41
  describe('deleting sections', () => {
31
42
  it('should delete a section by ID', async () => {
32
43
  mockTodoistApi.deleteSection.mockResolvedValue(true);
33
- const result = await deleteOne.execute({ type: 'section', id: 'section-123' }, mockTodoistApi);
34
- // Verify API was called correctly
44
+ const result = await deleteObject.execute({ type: 'section', id: 'section-123' }, mockTodoistApi);
35
45
  expect(mockTodoistApi.deleteSection).toHaveBeenCalledWith('section-123');
36
46
  expect(mockTodoistApi.deleteProject).not.toHaveBeenCalled();
37
47
  expect(mockTodoistApi.deleteTask).not.toHaveBeenCalled();
38
- // Verify success response
39
- expect(result).toEqual({ success: true });
48
+ const textContent = extractTextContent(result);
49
+ expect(textContent).toMatchSnapshot();
50
+ expect(textContent).toContain('Deleted section: id=section-123');
51
+ expect(textContent).toContain(`Use ${ToolNames.FIND_SECTIONS} to see remaining sections`);
52
+ expect(result.structuredContent).toEqual({
53
+ deletedEntity: { type: 'section', id: 'section-123' },
54
+ success: true,
55
+ });
40
56
  });
41
57
  it('should propagate section deletion errors', async () => {
42
58
  const apiError = new Error('API Error: Section not found');
43
59
  mockTodoistApi.deleteSection.mockRejectedValue(apiError);
44
- await expect(deleteOne.execute({ type: 'section', id: 'non-existent-section' }, mockTodoistApi)).rejects.toThrow('API Error: Section not found');
60
+ await expect(deleteObject.execute({ type: 'section', id: 'non-existent-section' }, mockTodoistApi)).rejects.toThrow('API Error: Section not found');
45
61
  });
46
62
  });
47
63
  describe('deleting tasks', () => {
48
64
  it('should delete a task by ID', async () => {
49
65
  mockTodoistApi.deleteTask.mockResolvedValue(true);
50
- const result = await deleteOne.execute({ type: 'task', id: '8485093748' }, mockTodoistApi);
51
- // Verify API was called correctly
66
+ const result = await deleteObject.execute({ type: 'task', id: '8485093748' }, mockTodoistApi);
52
67
  expect(mockTodoistApi.deleteTask).toHaveBeenCalledWith('8485093748');
53
68
  expect(mockTodoistApi.deleteProject).not.toHaveBeenCalled();
54
69
  expect(mockTodoistApi.deleteSection).not.toHaveBeenCalled();
55
- // Verify success response
56
- expect(result).toEqual({ success: true });
70
+ const textContent = extractTextContent(result);
71
+ expect(textContent).toMatchSnapshot();
72
+ expect(textContent).toContain('Deleted task: id=8485093748');
73
+ expect(textContent).toContain(`Use ${FIND_TASKS_BY_DATE} to see remaining tasks`);
74
+ expect(result.structuredContent).toEqual({
75
+ deletedEntity: { type: 'task', id: '8485093748' },
76
+ success: true,
77
+ });
57
78
  });
58
79
  it('should propagate task deletion errors', async () => {
59
80
  const apiError = new Error('API Error: Task not found');
60
81
  mockTodoistApi.deleteTask.mockRejectedValue(apiError);
61
- await expect(deleteOne.execute({ type: 'task', id: 'non-existent-task' }, mockTodoistApi)).rejects.toThrow('API Error: Task not found');
82
+ await expect(deleteObject.execute({ type: 'task', id: 'non-existent-task' }, mockTodoistApi)).rejects.toThrow('API Error: Task not found');
62
83
  });
63
84
  it('should handle permission errors', async () => {
64
85
  const apiError = new Error('API Error: Insufficient permissions to delete task');
65
86
  mockTodoistApi.deleteTask.mockRejectedValue(apiError);
66
- await expect(deleteOne.execute({ type: 'task', id: 'restricted-task' }, mockTodoistApi)).rejects.toThrow('API Error: Insufficient permissions to delete task');
87
+ await expect(deleteObject.execute({ type: 'task', id: 'restricted-task' }, mockTodoistApi)).rejects.toThrow('API Error: Insufficient permissions to delete task');
67
88
  });
68
89
  });
69
90
  describe('type validation', () => {
70
91
  it('should handle all supported entity types', async () => {
71
- // Test all three supported types work correctly
72
92
  mockTodoistApi.deleteProject.mockResolvedValue(true);
73
93
  mockTodoistApi.deleteSection.mockResolvedValue(true);
74
94
  mockTodoistApi.deleteTask.mockResolvedValue(true);
75
95
  // Delete project
76
- await deleteOne.execute({ type: 'project', id: 'proj-1' }, mockTodoistApi);
96
+ await deleteObject.execute({ type: 'project', id: 'proj-1' }, mockTodoistApi);
77
97
  expect(mockTodoistApi.deleteProject).toHaveBeenCalledWith('proj-1');
78
98
  // Delete section
79
- await deleteOne.execute({ type: 'section', id: 'sect-1' }, mockTodoistApi);
99
+ await deleteObject.execute({ type: 'section', id: 'sect-1' }, mockTodoistApi);
80
100
  expect(mockTodoistApi.deleteSection).toHaveBeenCalledWith('sect-1');
81
101
  // Delete task
82
- await deleteOne.execute({ type: 'task', id: 'task-1' }, mockTodoistApi);
102
+ await deleteObject.execute({ type: 'task', id: 'task-1' }, mockTodoistApi);
83
103
  expect(mockTodoistApi.deleteTask).toHaveBeenCalledWith('task-1');
84
104
  // Verify each API method was called exactly once
85
105
  expect(mockTodoistApi.deleteProject).toHaveBeenCalledTimes(1);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=find-completed-tasks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-completed-tasks.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/find-completed-tasks.test.ts"],"names":[],"mappings":""}
@@ -1,12 +1,14 @@
1
1
  import { jest } from '@jest/globals';
2
- import { tasksListCompleted } from '../tasks-list-completed.js';
3
- import { createMockTask } from '../test-helpers.js';
2
+ import { createMockTask, extractTextContent } from '../../utils/test-helpers.js';
3
+ import { ToolNames } from '../../utils/tool-names.js';
4
+ import { findCompletedTasks } from '../find-completed-tasks.js';
4
5
  // Mock the Todoist API
5
6
  const mockTodoistApi = {
6
7
  getCompletedTasksByCompletionDate: jest.fn(),
7
8
  getCompletedTasksByDueDate: jest.fn(),
8
9
  };
9
- describe('tasks-list-completed tool', () => {
10
+ const { FIND_COMPLETED_TASKS } = ToolNames;
11
+ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
10
12
  beforeEach(() => {
11
13
  jest.clearAllMocks();
12
14
  });
@@ -35,32 +37,20 @@ describe('tasks-list-completed tool', () => {
35
37
  items: mockCompletedTasks,
36
38
  nextCursor: null,
37
39
  });
38
- const result = await tasksListCompleted.execute({ getBy: 'completion', limit: 50, since: '2025-08-10', until: '2025-08-15' }, mockTodoistApi);
40
+ const result = await findCompletedTasks.execute({ getBy: 'completion', limit: 50, since: '2025-08-10', until: '2025-08-15' }, mockTodoistApi);
39
41
  expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
40
42
  since: '2025-08-10',
41
43
  until: '2025-08-15',
42
44
  limit: 50,
43
45
  });
44
- expect(result).toEqual({
45
- tasks: [
46
- expect.objectContaining({
47
- id: '8485093748',
48
- content: 'Completed task 1',
49
- description: 'Task completed yesterday',
50
- dueDate: '2025-08-14',
51
- priority: 2,
52
- labels: ['work'],
53
- }),
54
- ],
55
- nextCursor: null,
56
- });
46
+ expect(extractTextContent(result)).toMatchSnapshot();
57
47
  });
58
48
  it('should handle explicit completion date query', async () => {
59
49
  mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue({
60
50
  items: [],
61
51
  nextCursor: 'next-cursor',
62
52
  });
63
- const result = await tasksListCompleted.execute({
53
+ const result = await findCompletedTasks.execute({
64
54
  getBy: 'completion',
65
55
  limit: 100,
66
56
  since: '2025-08-01',
@@ -75,7 +65,7 @@ describe('tasks-list-completed tool', () => {
75
65
  limit: 100,
76
66
  cursor: 'current-cursor',
77
67
  });
78
- expect(result).toEqual({ tasks: [], nextCursor: 'next-cursor' });
68
+ expect(extractTextContent(result)).toMatchSnapshot();
79
69
  });
80
70
  });
81
71
  describe('getting completed tasks by due date', () => {
@@ -103,7 +93,7 @@ describe('tasks-list-completed tool', () => {
103
93
  items: mockCompletedTasks,
104
94
  nextCursor: null,
105
95
  });
106
- const result = await tasksListCompleted.execute({
96
+ const result = await findCompletedTasks.execute({
107
97
  getBy: 'due',
108
98
  limit: 50,
109
99
  since: '2025-08-10',
@@ -115,34 +105,21 @@ describe('tasks-list-completed tool', () => {
115
105
  limit: 50,
116
106
  });
117
107
  expect(mockTodoistApi.getCompletedTasksByCompletionDate).not.toHaveBeenCalled();
118
- expect(result).toEqual({
119
- tasks: [
120
- expect.objectContaining({
121
- id: '8485093750',
122
- content: 'Task completed by due date',
123
- description: 'This task was due and completed',
124
- dueDate: '2025-08-15',
125
- recurring: 'every Monday',
126
- priority: 3,
127
- labels: ['urgent'],
128
- }),
129
- ],
130
- nextCursor: null,
131
- });
108
+ expect(extractTextContent(result)).toMatchSnapshot();
132
109
  });
133
110
  });
134
111
  describe('error handling', () => {
135
112
  it('should propagate completion date API errors', async () => {
136
113
  const apiError = new Error('API Error: Invalid date range');
137
114
  mockTodoistApi.getCompletedTasksByCompletionDate.mockRejectedValue(apiError);
138
- await expect(tasksListCompleted.execute(
115
+ await expect(findCompletedTasks.execute(
139
116
  // invalid date range
140
117
  { getBy: 'completion', limit: 50, since: '2025-08-31', until: '2025-08-01' }, mockTodoistApi)).rejects.toThrow('API Error: Invalid date range');
141
118
  });
142
119
  it('should propagate due date API errors', async () => {
143
120
  const apiError = new Error('API Error: Project not found');
144
121
  mockTodoistApi.getCompletedTasksByDueDate.mockRejectedValue(apiError);
145
- await expect(tasksListCompleted.execute({
122
+ await expect(findCompletedTasks.execute({
146
123
  getBy: 'due',
147
124
  limit: 50,
148
125
  since: '2025-08-01',
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=find-projects.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-projects.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/find-projects.test.ts"],"names":[],"mappings":""}
@@ -1,11 +1,13 @@
1
1
  import { jest } from '@jest/globals';
2
- import { projectsList } from '../projects-list.js';
3
- import { TEST_ERRORS, TEST_IDS, createMockApiResponse, createMockProject } from '../test-helpers.js';
2
+ import { TEST_ERRORS, TEST_IDS, createMockApiResponse, createMockProject, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
3
+ import { ToolNames } from '../../utils/tool-names.js';
4
+ import { findProjects } from '../find-projects.js';
4
5
  // Mock the Todoist API
5
6
  const mockTodoistApi = {
6
7
  getProjects: jest.fn(),
7
8
  };
8
- describe('projects-list tool', () => {
9
+ const { FIND_PROJECTS } = ToolNames;
10
+ describe(`${FIND_PROJECTS} tool`, () => {
9
11
  beforeEach(() => {
10
12
  jest.clearAllMocks();
11
13
  });
@@ -38,37 +40,27 @@ describe('projects-list tool', () => {
38
40
  }),
39
41
  ];
40
42
  mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
41
- const result = await projectsList.execute({ limit: 50 }, mockTodoistApi);
43
+ const result = await findProjects.execute({ limit: 50 }, mockTodoistApi);
42
44
  // Verify API was called correctly
43
45
  expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({
44
46
  limit: 50,
45
47
  cursor: null,
46
48
  });
47
- // Verify result is properly mapped
48
- expect(result).toEqual({
49
- projects: [
50
- expect.objectContaining({
51
- id: TEST_IDS.PROJECT_INBOX,
52
- name: 'Inbox',
53
- color: 'grey',
54
- inboxProject: true,
55
- }),
56
- expect.objectContaining({
57
- id: TEST_IDS.PROJECT_TEST,
58
- name: 'test-abc123def456-project',
59
- color: 'charcoal',
60
- }),
61
- expect.objectContaining({
62
- id: TEST_IDS.PROJECT_WORK,
63
- name: 'Work Project',
64
- color: 'blue',
65
- isFavorite: true,
66
- isShared: true,
67
- viewStyle: 'board',
68
- }),
69
- ],
49
+ expect(extractTextContent(result)).toMatchSnapshot();
50
+ // Verify structured content
51
+ const structuredContent = extractStructuredContent(result);
52
+ expect(structuredContent).toEqual(expect.objectContaining({
53
+ projects: expect.any(Array),
54
+ totalCount: 3,
55
+ hasMore: false,
70
56
  nextCursor: null,
71
- });
57
+ appliedFilters: {
58
+ search: undefined,
59
+ limit: 50,
60
+ cursor: undefined,
61
+ },
62
+ }));
63
+ expect(structuredContent.projects).toHaveLength(3);
72
64
  });
73
65
  it('should handle pagination with limit and cursor', async () => {
74
66
  const mockProject = createMockProject({
@@ -77,14 +69,23 @@ describe('projects-list tool', () => {
77
69
  color: 'red',
78
70
  });
79
71
  mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([mockProject], 'next-page-cursor'));
80
- const result = await projectsList.execute({ limit: 10, cursor: 'current-page-cursor' }, mockTodoistApi);
72
+ const result = await findProjects.execute({ limit: 10, cursor: 'current-page-cursor' }, mockTodoistApi);
81
73
  expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({
82
74
  limit: 10,
83
75
  cursor: 'current-page-cursor',
84
76
  });
85
- expect(result.projects).toHaveLength(1);
86
- expect(result.projects[0]?.name).toBe('First Project');
87
- expect(result.nextCursor).toBe('next-page-cursor');
77
+ expect(extractTextContent(result)).toMatchSnapshot();
78
+ // Verify structured content
79
+ const structuredContent = extractStructuredContent(result);
80
+ expect(structuredContent.projects).toHaveLength(1);
81
+ expect(structuredContent.totalCount).toBe(1);
82
+ expect(structuredContent.hasMore).toBe(true);
83
+ expect(structuredContent.nextCursor).toBe('next-page-cursor');
84
+ expect(structuredContent.appliedFilters).toEqual({
85
+ search: undefined,
86
+ limit: 10,
87
+ cursor: 'current-page-cursor',
88
+ });
88
89
  });
89
90
  });
90
91
  describe('searching projects', () => {
@@ -103,10 +104,20 @@ describe('projects-list tool', () => {
103
104
  createMockProject({ id: 'hobby-project-id', name: 'Hobby Work', color: 'orange' }),
104
105
  ];
105
106
  mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
106
- const result = await projectsList.execute({ search: 'work', limit: 50 }, mockTodoistApi);
107
+ const result = await findProjects.execute({ search: 'work', limit: 50 }, mockTodoistApi);
107
108
  expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({ limit: 50, cursor: null });
108
- expect(result.projects).toHaveLength(2);
109
- expect(result.projects.map((p) => p.name)).toEqual(['Work Project', 'Hobby Work']);
109
+ expect(extractTextContent(result)).toMatchSnapshot();
110
+ // Verify structured content with search filter
111
+ const structuredContent = extractStructuredContent(result);
112
+ expect(structuredContent.projects).toHaveLength(2); // Should match filtered results
113
+ expect(structuredContent.totalCount).toBe(2);
114
+ expect(structuredContent.hasMore).toBe(false);
115
+ expect(structuredContent.nextCursor).toBeNull();
116
+ expect(structuredContent.appliedFilters).toEqual({
117
+ search: 'work',
118
+ limit: 50,
119
+ cursor: undefined,
120
+ });
110
121
  });
111
122
  it.each([
112
123
  {
@@ -121,11 +132,16 @@ describe('projects-list tool', () => {
121
132
  expectedCount: 1,
122
133
  description: 'case insensitive matching',
123
134
  },
124
- ])('should handle search with $description', async ({ search, projects, expectedCount }) => {
135
+ ])('should handle search with $description', async ({ search, projects }) => {
125
136
  const mockProjects = projects.map((name) => createMockProject({ name }));
126
137
  mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
127
- const result = await projectsList.execute({ search, limit: 50 }, mockTodoistApi);
128
- expect(result.projects).toHaveLength(expectedCount);
138
+ const result = await findProjects.execute({ search, limit: 50 }, mockTodoistApi);
139
+ expect(extractTextContent(result)).toMatchSnapshot();
140
+ // Verify structured content
141
+ const structuredContent = extractStructuredContent(result);
142
+ expect(structuredContent).toEqual(expect.objectContaining({
143
+ appliedFilters: expect.objectContaining({ search }),
144
+ }));
129
145
  });
130
146
  });
131
147
  describe('error handling', () => {
@@ -134,7 +150,7 @@ describe('projects-list tool', () => {
134
150
  { error: TEST_ERRORS.INVALID_CURSOR, params: { cursor: 'invalid-cursor', limit: 50 } },
135
151
  ])('should propagate $error', async ({ error, params }) => {
136
152
  mockTodoistApi.getProjects.mockRejectedValue(new Error(error));
137
- await expect(projectsList.execute(params, mockTodoistApi)).rejects.toThrow(error);
153
+ await expect(findProjects.execute(params, mockTodoistApi)).rejects.toThrow(error);
138
154
  });
139
155
  });
140
156
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=find-sections.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-sections.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/find-sections.test.ts"],"names":[],"mappings":""}