@doist/todoist-ai 4.1.0 → 4.5.1
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 +405 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -16
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-helpers.js +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +80 -17
- package/dist/tool-helpers.d.ts +4 -0
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +2 -0
- package/dist/tools/__tests__/add-projects.test.js +1 -1
- package/dist/tools/__tests__/add-sections.test.js +1 -1
- package/dist/tools/__tests__/add-tasks.test.js +52 -13
- package/dist/tools/__tests__/assignment-integration.test.d.ts +2 -0
- package/dist/tools/__tests__/assignment-integration.test.d.ts.map +1 -0
- package/dist/tools/__tests__/assignment-integration.test.js +415 -0
- package/dist/tools/__tests__/find-completed-tasks.test.js +136 -2
- package/dist/tools/__tests__/find-projects.test.js +1 -1
- package/dist/tools/__tests__/find-sections.test.js +1 -1
- package/dist/tools/__tests__/find-tasks-by-date.test.js +122 -3
- package/dist/tools/__tests__/find-tasks.test.js +258 -11
- package/dist/tools/__tests__/get-overview.test.js +1 -1
- package/dist/tools/__tests__/update-sections.test.js +1 -0
- package/dist/tools/__tests__/update-tasks.test.js +6 -6
- 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 +28 -5
- package/dist/tools/add-comments.d.ts.map +1 -1
- package/dist/tools/add-comments.js +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-projects.js +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-sections.js +1 -1
- package/dist/tools/add-tasks.d.ts +16 -10
- package/dist/tools/add-tasks.d.ts.map +1 -1
- package/dist/tools/add-tasks.js +49 -3
- package/dist/tools/find-comments.d.ts +27 -4
- package/dist/tools/find-comments.d.ts.map +1 -1
- package/dist/tools/find-completed-tasks.d.ts +12 -4
- package/dist/tools/find-completed-tasks.d.ts.map +1 -1
- package/dist/tools/find-completed-tasks.js +20 -4
- package/dist/tools/find-project-collaborators.d.ts +64 -0
- package/dist/tools/find-project-collaborators.d.ts.map +1 -0
- package/dist/tools/find-project-collaborators.js +151 -0
- package/dist/tools/find-tasks-by-date.d.ts +8 -0
- package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
- package/dist/tools/find-tasks-by-date.js +19 -2
- package/dist/tools/find-tasks.d.ts +13 -2
- package/dist/tools/find-tasks.d.ts.map +1 -1
- package/dist/tools/find-tasks.js +172 -23
- package/dist/tools/get-overview.d.ts +2 -2
- package/dist/tools/get-overview.d.ts.map +1 -1
- package/dist/tools/get-overview.js +1 -1
- package/dist/tools/manage-assignments.d.ts +52 -0
- package/dist/tools/manage-assignments.d.ts.map +1 -0
- package/dist/tools/manage-assignments.js +337 -0
- package/dist/tools/update-comments.d.ts +25 -2
- package/dist/tools/update-comments.d.ts.map +1 -1
- package/dist/tools/update-comments.js +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-sections.js +1 -1
- package/dist/tools/update-tasks.d.ts +16 -10
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/tools/update-tasks.js +32 -9
- 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/assignment-validator.d.ts +69 -0
- package/dist/utils/assignment-validator.d.ts.map +1 -0
- package/dist/utils/assignment-validator.js +253 -0
- package/dist/utils/duration-parser.d.ts +2 -2
- package/dist/utils/duration-parser.d.ts.map +1 -1
- 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/priorities.d.ts +8 -0
- package/dist/utils/priorities.d.ts.map +1 -0
- package/dist/utils/priorities.js +15 -0
- package/dist/utils/response-builders.d.ts +2 -2
- package/dist/utils/response-builders.d.ts.map +1 -1
- package/dist/utils/response-builders.js +8 -1
- package/dist/utils/test-helpers.d.ts +2 -0
- package/dist/utils/test-helpers.d.ts.map +1 -1
- package/dist/utils/test-helpers.js +3 -0
- package/dist/utils/tool-names.d.ts +3 -0
- package/dist/utils/tool-names.d.ts.map +1 -1
- package/dist/utils/tool-names.js +4 -0
- package/dist/utils/user-resolver.d.ts +39 -0
- package/dist/utils/user-resolver.d.ts.map +1 -0
- package/dist/utils/user-resolver.js +179 -0
- package/package.json +7 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
2
|
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
-
import {
|
|
3
|
+
import { createMappedTask, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
|
|
4
4
|
import { ToolNames } from '../../utils/tool-names.js';
|
|
5
5
|
import { findTasksByDate } from '../find-tasks-by-date.js';
|
|
6
6
|
// Mock the tool helpers
|
|
@@ -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
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
2
|
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
-
import {
|
|
3
|
+
import { createMappedTask, createMockApiResponse, createMockTask, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, TODAY, } from '../../utils/test-helpers.js';
|
|
4
4
|
import { ToolNames } from '../../utils/tool-names.js';
|
|
5
5
|
import { findTasks } from '../find-tasks.js';
|
|
6
6
|
jest.mock('../../tool-helpers', () => {
|
|
@@ -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, responsibleUser, 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, responsibleUser, 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 }) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
|
-
import {
|
|
2
|
+
import { createMockProject, createMockSection, createMockTask, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
|
|
3
3
|
import { ToolNames } from '../../utils/tool-names.js';
|
|
4
4
|
import { getOverview } from '../get-overview.js';
|
|
5
5
|
// Mock the Todoist API
|
|
@@ -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);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
|
-
import {
|
|
2
|
+
import { createMockTask, extractStructuredContent, extractTextContent, TEST_IDS, } from '../../utils/test-helpers.js';
|
|
3
3
|
import { ToolNames } from '../../utils/tool-names.js';
|
|
4
4
|
import { updateTasks } from '../update-tasks.js';
|
|
5
5
|
// Mock the Todoist API
|
|
@@ -109,13 +109,13 @@ describe(`${UPDATE_TASKS} tool`, () => {
|
|
|
109
109
|
tasks: [
|
|
110
110
|
{
|
|
111
111
|
id: '8485093749',
|
|
112
|
-
priority:
|
|
112
|
+
priority: 'p3',
|
|
113
113
|
dueString: 'Aug 20',
|
|
114
114
|
},
|
|
115
115
|
],
|
|
116
116
|
}, mockTodoistApi);
|
|
117
117
|
expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093749', {
|
|
118
|
-
priority:
|
|
118
|
+
priority: 2,
|
|
119
119
|
dueString: 'Aug 20',
|
|
120
120
|
});
|
|
121
121
|
// Verify result structure
|
|
@@ -208,7 +208,7 @@ describe(`${UPDATE_TASKS} tool`, () => {
|
|
|
208
208
|
id: '8485093752',
|
|
209
209
|
content: 'Completely updated task',
|
|
210
210
|
description: 'New description with details',
|
|
211
|
-
priority:
|
|
211
|
+
priority: 'p4',
|
|
212
212
|
dueString: 'every Friday',
|
|
213
213
|
projectId: 'different-project-id',
|
|
214
214
|
},
|
|
@@ -222,7 +222,7 @@ describe(`${UPDATE_TASKS} tool`, () => {
|
|
|
222
222
|
expect(mockTodoistApi.updateTask).toHaveBeenCalledWith('8485093752', {
|
|
223
223
|
content: 'Completely updated task',
|
|
224
224
|
description: 'New description with details',
|
|
225
|
-
priority:
|
|
225
|
+
priority: 1,
|
|
226
226
|
dueString: 'every Friday',
|
|
227
227
|
});
|
|
228
228
|
// Verify result structure
|
|
@@ -391,7 +391,7 @@ describe(`${UPDATE_TASKS} tool`, () => {
|
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
error: 'API Error: Invalid priority value',
|
|
394
|
-
params: { id: '8485093748',
|
|
394
|
+
params: { id: '8485093748', content: 'Test task' },
|
|
395
395
|
},
|
|
396
396
|
])('should propagate $error', async ({ error, params }) => {
|
|
397
397
|
mockTodoistApi.updateTask.mockRejectedValue(new Error(error));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-info.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/user-info.test.ts"],"names":[],"mappings":""}
|