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