@doist/todoist-ai 2.0.0 → 2.0.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/README.md +7 -0
- package/dist/index.d.ts +18 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -48
- package/dist/main.js +6 -11
- package/dist/mcp-helpers.d.ts +2 -2
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-helpers.js +1 -4
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +34 -36
- package/dist/todoist-tool.js +1 -2
- package/dist/tool-helpers.d.ts +11 -1
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +41 -22
- package/dist/tool-helpers.test.js +35 -14
- package/dist/tools/__tests__/delete-one.test.d.ts +2 -0
- package/dist/tools/__tests__/delete-one.test.d.ts.map +1 -0
- package/dist/tools/__tests__/delete-one.test.js +90 -0
- package/dist/tools/__tests__/overview.test.d.ts +2 -0
- package/dist/tools/__tests__/overview.test.d.ts.map +1 -0
- package/dist/tools/__tests__/overview.test.js +163 -0
- package/dist/tools/__tests__/projects-list.test.d.ts +2 -0
- package/dist/tools/__tests__/projects-list.test.d.ts.map +1 -0
- package/dist/tools/__tests__/projects-list.test.js +140 -0
- package/dist/tools/__tests__/projects-manage.test.d.ts +2 -0
- package/dist/tools/__tests__/projects-manage.test.d.ts.map +1 -0
- package/dist/tools/__tests__/projects-manage.test.js +106 -0
- package/dist/tools/__tests__/sections-manage.test.d.ts +2 -0
- package/dist/tools/__tests__/sections-manage.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sections-manage.test.js +138 -0
- package/dist/tools/__tests__/sections-search.test.d.ts +2 -0
- package/dist/tools/__tests__/sections-search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sections-search.test.js +235 -0
- package/dist/tools/__tests__/tasks-add-multiple.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-add-multiple.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-add-multiple.test.js +160 -0
- package/dist/tools/__tests__/tasks-complete-multiple.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-complete-multiple.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-complete-multiple.test.js +146 -0
- package/dist/tools/__tests__/tasks-list-by-date.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-list-by-date.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-list-by-date.test.js +192 -0
- package/dist/tools/__tests__/tasks-list-completed.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-list-completed.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-list-completed.test.js +154 -0
- package/dist/tools/__tests__/tasks-list-for-container.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-list-for-container.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-list-for-container.test.js +232 -0
- package/dist/tools/__tests__/tasks-organize-multiple.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-organize-multiple.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-organize-multiple.test.js +245 -0
- package/dist/tools/__tests__/tasks-search.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-search.test.js +106 -0
- package/dist/tools/__tests__/tasks-update-one.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-update-one.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-update-one.test.js +161 -0
- package/dist/tools/delete-one.js +4 -7
- package/dist/tools/overview.js +8 -11
- package/dist/tools/projects-list.js +7 -10
- package/dist/tools/projects-manage.js +6 -9
- package/dist/tools/sections-manage.js +7 -10
- package/dist/tools/sections-search.js +4 -7
- package/dist/tools/tasks-add-multiple.js +13 -16
- package/dist/tools/tasks-complete-multiple.js +3 -6
- package/dist/tools/tasks-list-by-date.js +12 -15
- package/dist/tools/tasks-list-completed.d.ts +1 -1
- package/dist/tools/tasks-list-completed.js +13 -16
- package/dist/tools/tasks-list-for-container.js +8 -11
- package/dist/tools/tasks-organize-multiple.d.ts.map +1 -1
- package/dist/tools/tasks-organize-multiple.js +20 -14
- package/dist/tools/tasks-search.js +7 -10
- package/dist/tools/tasks-update-one.d.ts +2 -2
- package/dist/tools/tasks-update-one.d.ts.map +1 -1
- package/dist/tools/tasks-update-one.js +22 -15
- package/dist/tools/test-helpers.d.ts +79 -0
- package/dist/tools/test-helpers.d.ts.map +1 -0
- package/dist/tools/test-helpers.js +139 -0
- package/package.json +6 -2
- package/scripts/test-executable.cjs +69 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
+
import { tasksListByDate } from '../tasks-list-by-date.js';
|
|
4
|
+
import { TEST_ERRORS, TEST_IDS, createMappedTask } from '../test-helpers.js';
|
|
5
|
+
// Mock the tool helpers
|
|
6
|
+
jest.mock('../../tool-helpers', () => ({
|
|
7
|
+
getTasksByFilter: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
const mockGetTasksByFilter = getTasksByFilter;
|
|
10
|
+
// Mock the Todoist API (not directly used by tasks-list-by-date, but needed for type)
|
|
11
|
+
const mockTodoistApi = {};
|
|
12
|
+
// Mock date-fns functions to make tests deterministic
|
|
13
|
+
jest.mock('date-fns', () => ({
|
|
14
|
+
addDays: jest.fn(() => new Date('2025-08-16')), // Return predictable end date
|
|
15
|
+
formatISO: jest.fn((date, options) => {
|
|
16
|
+
if (typeof date === 'string') {
|
|
17
|
+
return date; // Return string dates as-is
|
|
18
|
+
}
|
|
19
|
+
if (options &&
|
|
20
|
+
typeof options === 'object' &&
|
|
21
|
+
'representation' in options &&
|
|
22
|
+
options.representation === 'date') {
|
|
23
|
+
return '2025-08-15'; // Return predictable date for 'today'
|
|
24
|
+
}
|
|
25
|
+
return '2025-08-16'; // Return predictable end date
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
describe('tasks-list-by-date tool', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
// Mock current date to make tests deterministic
|
|
32
|
+
jest.spyOn(Date, 'now').mockReturnValue(new Date('2025-08-15T10:00:00Z').getTime());
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
jest.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
describe('listing overdue tasks', () => {
|
|
38
|
+
it.each([
|
|
39
|
+
{ daysCount: 7, hasTasks: true, description: 'with tasks' },
|
|
40
|
+
{ daysCount: 5, hasTasks: false, description: 'ignoring daysCount' },
|
|
41
|
+
])('should handle overdue tasks $description', async ({ daysCount, hasTasks }) => {
|
|
42
|
+
const mockTasks = hasTasks
|
|
43
|
+
? [
|
|
44
|
+
createMappedTask({
|
|
45
|
+
id: TEST_IDS.TASK_1,
|
|
46
|
+
content: 'Overdue task',
|
|
47
|
+
dueDate: '2025-08-10',
|
|
48
|
+
priority: 2,
|
|
49
|
+
labels: ['urgent'],
|
|
50
|
+
}),
|
|
51
|
+
]
|
|
52
|
+
: [];
|
|
53
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
54
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
55
|
+
const result = await tasksListByDate.execute({ startDate: 'overdue', limit: 50, daysCount }, mockTodoistApi);
|
|
56
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
57
|
+
client: mockTodoistApi,
|
|
58
|
+
query: 'overdue',
|
|
59
|
+
cursor: undefined,
|
|
60
|
+
limit: 50,
|
|
61
|
+
});
|
|
62
|
+
expect(result).toEqual(mockResponse);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('listing tasks by date range', () => {
|
|
66
|
+
it('should get tasks for today when startDate is "today"', async () => {
|
|
67
|
+
const mockTasks = [createMappedTask({ content: 'Today task', dueDate: '2025-08-15' })];
|
|
68
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
69
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
70
|
+
const result = await tasksListByDate.execute({ startDate: 'today', limit: 50, daysCount: 7 }, mockTodoistApi);
|
|
71
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
72
|
+
client: mockTodoistApi,
|
|
73
|
+
query: expect.stringContaining('due after:') && expect.stringContaining('due before:'),
|
|
74
|
+
cursor: undefined,
|
|
75
|
+
limit: 50,
|
|
76
|
+
});
|
|
77
|
+
expect(result).toEqual(mockResponse);
|
|
78
|
+
});
|
|
79
|
+
it.each([
|
|
80
|
+
{
|
|
81
|
+
name: 'specific date',
|
|
82
|
+
params: { startDate: '2025-08-20', limit: 50, daysCount: 7 },
|
|
83
|
+
tasks: [createMappedTask({ content: 'Specific date task', dueDate: '2025-08-20' })],
|
|
84
|
+
cursor: null,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'multiple days with pagination',
|
|
88
|
+
params: {
|
|
89
|
+
startDate: '2025-08-20',
|
|
90
|
+
daysCount: 3,
|
|
91
|
+
limit: 20,
|
|
92
|
+
cursor: 'current-cursor',
|
|
93
|
+
},
|
|
94
|
+
tasks: [
|
|
95
|
+
createMappedTask({
|
|
96
|
+
id: TEST_IDS.TASK_2,
|
|
97
|
+
content: 'Multi-day task 1',
|
|
98
|
+
dueDate: '2025-08-20',
|
|
99
|
+
}),
|
|
100
|
+
createMappedTask({
|
|
101
|
+
id: TEST_IDS.TASK_3,
|
|
102
|
+
content: 'Multi-day task 2',
|
|
103
|
+
dueDate: '2025-08-21',
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
cursor: 'next-page-cursor',
|
|
107
|
+
},
|
|
108
|
+
])('should handle $name', async ({ params, tasks, cursor }) => {
|
|
109
|
+
const mockResponse = { tasks, nextCursor: cursor };
|
|
110
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
111
|
+
const result = await tasksListByDate.execute(params, mockTodoistApi);
|
|
112
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
113
|
+
client: mockTodoistApi,
|
|
114
|
+
query: expect.stringContaining('2025-08-20'),
|
|
115
|
+
cursor: params.cursor || undefined,
|
|
116
|
+
limit: params.limit,
|
|
117
|
+
});
|
|
118
|
+
expect(result).toEqual(mockResponse);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('pagination and limits', () => {
|
|
122
|
+
it.each([
|
|
123
|
+
{
|
|
124
|
+
name: 'pagination parameters',
|
|
125
|
+
params: {
|
|
126
|
+
startDate: 'today',
|
|
127
|
+
limit: 25,
|
|
128
|
+
daysCount: 7,
|
|
129
|
+
cursor: 'pagination-cursor',
|
|
130
|
+
},
|
|
131
|
+
expectedCursor: 'pagination-cursor',
|
|
132
|
+
expectedLimit: 25,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'default values',
|
|
136
|
+
params: { startDate: '2025-08-15', limit: 50, daysCount: 7 },
|
|
137
|
+
expectedCursor: undefined,
|
|
138
|
+
expectedLimit: 50,
|
|
139
|
+
},
|
|
140
|
+
])('should handle $name', async ({ params, expectedCursor, expectedLimit }) => {
|
|
141
|
+
const mockResponse = { tasks: [], nextCursor: null };
|
|
142
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
143
|
+
await tasksListByDate.execute(params, mockTodoistApi);
|
|
144
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
145
|
+
client: mockTodoistApi,
|
|
146
|
+
query: expect.any(String),
|
|
147
|
+
cursor: expectedCursor,
|
|
148
|
+
limit: expectedLimit,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('edge cases', () => {
|
|
153
|
+
it.each([
|
|
154
|
+
{ name: 'empty results', daysCount: 7, shouldReturnResult: true },
|
|
155
|
+
{ name: 'maximum daysCount', daysCount: 30, shouldReturnResult: false },
|
|
156
|
+
{ name: 'minimum daysCount', daysCount: 1, shouldReturnResult: false },
|
|
157
|
+
])('should handle $name', async ({ daysCount, shouldReturnResult }) => {
|
|
158
|
+
const mockResponse = { tasks: [], nextCursor: null };
|
|
159
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
160
|
+
const startDate = daysCount === 7 ? 'today' : '2025-08-15';
|
|
161
|
+
const result = await tasksListByDate.execute({ startDate, limit: 50, daysCount }, mockTodoistApi);
|
|
162
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledTimes(1);
|
|
163
|
+
if (shouldReturnResult) {
|
|
164
|
+
expect(result).toEqual(mockResponse);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('error handling', () => {
|
|
169
|
+
it.each([
|
|
170
|
+
{
|
|
171
|
+
error: TEST_ERRORS.INVALID_FILTER,
|
|
172
|
+
params: { startDate: 'today', limit: 50, daysCount: 7 },
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
error: TEST_ERRORS.API_RATE_LIMIT,
|
|
176
|
+
params: { startDate: 'overdue', limit: 50, daysCount: 7 },
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
error: TEST_ERRORS.INVALID_CURSOR,
|
|
180
|
+
params: {
|
|
181
|
+
startDate: '2025-08-15',
|
|
182
|
+
limit: 50,
|
|
183
|
+
daysCount: 7,
|
|
184
|
+
cursor: 'invalid-cursor',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
])('should propagate $error', async ({ error, params }) => {
|
|
188
|
+
mockGetTasksByFilter.mockRejectedValue(new Error(error));
|
|
189
|
+
await expect(tasksListByDate.execute(params, mockTodoistApi)).rejects.toThrow(error);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-list-completed.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-list-completed.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { tasksListCompleted } from '../tasks-list-completed.js';
|
|
3
|
+
import { createMockTask } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
getCompletedTasksByCompletionDate: jest.fn(),
|
|
7
|
+
getCompletedTasksByDueDate: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
describe('tasks-list-completed tool', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
describe('getting completed tasks by completion date (default)', () => {
|
|
14
|
+
it('should get completed tasks by completion date', async () => {
|
|
15
|
+
const mockCompletedTasks = [
|
|
16
|
+
createMockTask({
|
|
17
|
+
id: '8485093748',
|
|
18
|
+
content: 'Completed task 1',
|
|
19
|
+
description: 'Task completed yesterday',
|
|
20
|
+
completedAt: '2024-01-01T00:00:00Z',
|
|
21
|
+
labels: ['work'],
|
|
22
|
+
priority: 2,
|
|
23
|
+
url: 'https://todoist.com/showTask?id=8485093748',
|
|
24
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
25
|
+
due: {
|
|
26
|
+
date: '2025-08-14',
|
|
27
|
+
isRecurring: false,
|
|
28
|
+
lang: 'en',
|
|
29
|
+
string: 'Aug 14',
|
|
30
|
+
timezone: null,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
];
|
|
34
|
+
mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue({
|
|
35
|
+
items: mockCompletedTasks,
|
|
36
|
+
nextCursor: null,
|
|
37
|
+
});
|
|
38
|
+
const result = await tasksListCompleted.execute({ getBy: 'completion', limit: 50, since: '2025-08-10', until: '2025-08-15' }, mockTodoistApi);
|
|
39
|
+
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
40
|
+
since: '2025-08-10',
|
|
41
|
+
until: '2025-08-15',
|
|
42
|
+
limit: 50,
|
|
43
|
+
});
|
|
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
|
+
});
|
|
57
|
+
});
|
|
58
|
+
it('should handle explicit completion date query', async () => {
|
|
59
|
+
mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue({
|
|
60
|
+
items: [],
|
|
61
|
+
nextCursor: 'next-cursor',
|
|
62
|
+
});
|
|
63
|
+
const result = await tasksListCompleted.execute({
|
|
64
|
+
getBy: 'completion',
|
|
65
|
+
limit: 100,
|
|
66
|
+
since: '2025-08-01',
|
|
67
|
+
until: '2025-08-31',
|
|
68
|
+
projectId: 'specific-project-id',
|
|
69
|
+
cursor: 'current-cursor',
|
|
70
|
+
}, mockTodoistApi);
|
|
71
|
+
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
72
|
+
since: '2025-08-01',
|
|
73
|
+
until: '2025-08-31',
|
|
74
|
+
projectId: 'specific-project-id',
|
|
75
|
+
limit: 100,
|
|
76
|
+
cursor: 'current-cursor',
|
|
77
|
+
});
|
|
78
|
+
expect(result).toEqual({ tasks: [], nextCursor: 'next-cursor' });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('getting completed tasks by due date', () => {
|
|
82
|
+
it('should get completed tasks by due date', async () => {
|
|
83
|
+
const mockCompletedTasks = [
|
|
84
|
+
createMockTask({
|
|
85
|
+
id: '8485093750',
|
|
86
|
+
content: 'Task completed by due date',
|
|
87
|
+
description: 'This task was due and completed',
|
|
88
|
+
completedAt: '2024-01-01T00:00:00Z',
|
|
89
|
+
labels: ['urgent'],
|
|
90
|
+
priority: 3,
|
|
91
|
+
url: 'https://todoist.com/showTask?id=8485093750',
|
|
92
|
+
addedAt: '2025-08-13T22:09:58.123456Z',
|
|
93
|
+
due: {
|
|
94
|
+
date: '2025-08-15',
|
|
95
|
+
isRecurring: true,
|
|
96
|
+
lang: 'en',
|
|
97
|
+
string: 'every Monday',
|
|
98
|
+
timezone: null,
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
102
|
+
mockTodoistApi.getCompletedTasksByDueDate.mockResolvedValue({
|
|
103
|
+
items: mockCompletedTasks,
|
|
104
|
+
nextCursor: null,
|
|
105
|
+
});
|
|
106
|
+
const result = await tasksListCompleted.execute({
|
|
107
|
+
getBy: 'due',
|
|
108
|
+
limit: 50,
|
|
109
|
+
since: '2025-08-10',
|
|
110
|
+
until: '2025-08-20',
|
|
111
|
+
}, mockTodoistApi);
|
|
112
|
+
expect(mockTodoistApi.getCompletedTasksByDueDate).toHaveBeenCalledWith({
|
|
113
|
+
since: '2025-08-10',
|
|
114
|
+
until: '2025-08-20',
|
|
115
|
+
limit: 50,
|
|
116
|
+
});
|
|
117
|
+
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
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('error handling', () => {
|
|
135
|
+
it('should propagate completion date API errors', async () => {
|
|
136
|
+
const apiError = new Error('API Error: Invalid date range');
|
|
137
|
+
mockTodoistApi.getCompletedTasksByCompletionDate.mockRejectedValue(apiError);
|
|
138
|
+
await expect(tasksListCompleted.execute(
|
|
139
|
+
// invalid date range
|
|
140
|
+
{ getBy: 'completion', limit: 50, since: '2025-08-31', until: '2025-08-01' }, mockTodoistApi)).rejects.toThrow('API Error: Invalid date range');
|
|
141
|
+
});
|
|
142
|
+
it('should propagate due date API errors', async () => {
|
|
143
|
+
const apiError = new Error('API Error: Project not found');
|
|
144
|
+
mockTodoistApi.getCompletedTasksByDueDate.mockRejectedValue(apiError);
|
|
145
|
+
await expect(tasksListCompleted.execute({
|
|
146
|
+
getBy: 'due',
|
|
147
|
+
limit: 50,
|
|
148
|
+
since: '2025-08-01',
|
|
149
|
+
until: '2025-08-31',
|
|
150
|
+
projectId: 'non-existent-project',
|
|
151
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-list-for-container.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-list-for-container.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { tasksListForContainer } from '../tasks-list-for-container.js';
|
|
3
|
+
import { TEST_IDS, createMockApiResponse, createMockTask } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
getTasks: jest.fn(),
|
|
7
|
+
};
|
|
8
|
+
describe('tasks-list-for-container tool', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
describe('container types', () => {
|
|
13
|
+
it.each([
|
|
14
|
+
{
|
|
15
|
+
name: 'project',
|
|
16
|
+
type: 'project',
|
|
17
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
18
|
+
expectedParam: { projectId: TEST_IDS.PROJECT_TEST },
|
|
19
|
+
tasks: [createMockTask({ content: 'Project task' })],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'section',
|
|
23
|
+
type: 'section',
|
|
24
|
+
id: TEST_IDS.SECTION_1,
|
|
25
|
+
expectedParam: { sectionId: TEST_IDS.SECTION_1 },
|
|
26
|
+
tasks: [createMockTask({ content: 'Section task' })],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'parent task',
|
|
30
|
+
type: 'parent',
|
|
31
|
+
id: TEST_IDS.TASK_1,
|
|
32
|
+
expectedParam: { parentId: TEST_IDS.TASK_1 },
|
|
33
|
+
tasks: [createMockTask({ content: 'Subtask' })],
|
|
34
|
+
},
|
|
35
|
+
])('should get tasks for $name', async ({ type, id, expectedParam, tasks }) => {
|
|
36
|
+
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(tasks));
|
|
37
|
+
const result = await tasksListForContainer.execute({ type, id, limit: 10 }, mockTodoistApi);
|
|
38
|
+
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
39
|
+
limit: 10,
|
|
40
|
+
cursor: null,
|
|
41
|
+
...expectedParam,
|
|
42
|
+
});
|
|
43
|
+
expect(result.tasks).toHaveLength(1);
|
|
44
|
+
expect(result.nextCursor).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('getting tasks for a section', () => {
|
|
48
|
+
it('should get tasks for a specific section', async () => {
|
|
49
|
+
const mockTasks = [
|
|
50
|
+
createMockTask({
|
|
51
|
+
id: '8485093751',
|
|
52
|
+
content: 'Section task 1',
|
|
53
|
+
description: 'Task in specific section',
|
|
54
|
+
labels: ['urgent'],
|
|
55
|
+
childOrder: 1,
|
|
56
|
+
priority: 3,
|
|
57
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
58
|
+
sectionId: 'section-123',
|
|
59
|
+
url: 'https://todoist.com/showTask?id=8485093751',
|
|
60
|
+
addedAt: '2025-08-13T22:09:59.123456Z',
|
|
61
|
+
due: {
|
|
62
|
+
date: '2025-08-16',
|
|
63
|
+
isRecurring: true,
|
|
64
|
+
lang: 'en',
|
|
65
|
+
string: 'every week',
|
|
66
|
+
timezone: null,
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
];
|
|
70
|
+
mockTodoistApi.getTasks.mockResolvedValue({ results: mockTasks, nextCursor: null });
|
|
71
|
+
const result = await tasksListForContainer.execute({ type: 'section', id: 'section-123', limit: 15 }, mockTodoistApi);
|
|
72
|
+
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
73
|
+
limit: 15,
|
|
74
|
+
cursor: null,
|
|
75
|
+
sectionId: 'section-123',
|
|
76
|
+
});
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
tasks: [
|
|
79
|
+
expect.objectContaining({
|
|
80
|
+
id: '8485093751',
|
|
81
|
+
content: 'Section task 1',
|
|
82
|
+
description: 'Task in specific section',
|
|
83
|
+
dueDate: '2025-08-16',
|
|
84
|
+
recurring: 'every week',
|
|
85
|
+
priority: 3,
|
|
86
|
+
sectionId: 'section-123',
|
|
87
|
+
labels: ['urgent'],
|
|
88
|
+
}),
|
|
89
|
+
],
|
|
90
|
+
nextCursor: null,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
it('should handle empty section', async () => {
|
|
94
|
+
mockTodoistApi.getTasks.mockResolvedValue({ results: [], nextCursor: null });
|
|
95
|
+
const result = await tasksListForContainer.execute({ type: 'section', id: 'empty-section-id', limit: 10 }, mockTodoistApi);
|
|
96
|
+
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
97
|
+
limit: 10,
|
|
98
|
+
cursor: null,
|
|
99
|
+
sectionId: 'empty-section-id',
|
|
100
|
+
});
|
|
101
|
+
expect(result).toEqual({ tasks: [], nextCursor: null });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('getting subtasks for a parent task', () => {
|
|
105
|
+
it('should get subtasks for a specific parent task', async () => {
|
|
106
|
+
const mockSubtasks = [
|
|
107
|
+
createMockTask({
|
|
108
|
+
id: '8485093752',
|
|
109
|
+
content: 'Subtask 1',
|
|
110
|
+
description: 'First subtask',
|
|
111
|
+
childOrder: 1,
|
|
112
|
+
priority: 1,
|
|
113
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
114
|
+
parentId: 'parent-task-123',
|
|
115
|
+
url: 'https://todoist.com/showTask?id=8485093752',
|
|
116
|
+
addedAt: '2025-08-13T22:10:00.123456Z',
|
|
117
|
+
}),
|
|
118
|
+
createMockTask({
|
|
119
|
+
id: '8485093753',
|
|
120
|
+
content: 'Subtask 2',
|
|
121
|
+
description: 'Second subtask',
|
|
122
|
+
labels: ['follow-up'],
|
|
123
|
+
childOrder: 2,
|
|
124
|
+
priority: 2,
|
|
125
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
126
|
+
parentId: 'parent-task-123',
|
|
127
|
+
url: 'https://todoist.com/showTask?id=8485093753',
|
|
128
|
+
addedAt: '2025-08-13T22:10:01.123456Z',
|
|
129
|
+
due: {
|
|
130
|
+
date: '2025-08-18',
|
|
131
|
+
isRecurring: false,
|
|
132
|
+
lang: 'en',
|
|
133
|
+
string: 'Aug 18',
|
|
134
|
+
timezone: null,
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
];
|
|
138
|
+
mockTodoistApi.getTasks.mockResolvedValue({ results: mockSubtasks, nextCursor: null });
|
|
139
|
+
const result = await tasksListForContainer.execute({ type: 'parent', id: 'parent-task-123', limit: 10 }, mockTodoistApi);
|
|
140
|
+
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
141
|
+
parentId: 'parent-task-123',
|
|
142
|
+
cursor: null,
|
|
143
|
+
limit: 10,
|
|
144
|
+
});
|
|
145
|
+
expect(result).toEqual({
|
|
146
|
+
tasks: [
|
|
147
|
+
expect.objectContaining({
|
|
148
|
+
id: '8485093752',
|
|
149
|
+
content: 'Subtask 1',
|
|
150
|
+
description: 'First subtask',
|
|
151
|
+
parentId: 'parent-task-123',
|
|
152
|
+
labels: [],
|
|
153
|
+
}),
|
|
154
|
+
expect.objectContaining({
|
|
155
|
+
id: '8485093753',
|
|
156
|
+
content: 'Subtask 2',
|
|
157
|
+
description: 'Second subtask',
|
|
158
|
+
dueDate: '2025-08-18',
|
|
159
|
+
parentId: 'parent-task-123',
|
|
160
|
+
priority: 2,
|
|
161
|
+
labels: ['follow-up'],
|
|
162
|
+
}),
|
|
163
|
+
],
|
|
164
|
+
nextCursor: null,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
it('should handle parent task with no subtasks', async () => {
|
|
168
|
+
mockTodoistApi.getTasks.mockResolvedValue({ results: [], nextCursor: null });
|
|
169
|
+
const result = await tasksListForContainer.execute({ type: 'parent', id: 'parent-with-no-subtasks', limit: 10 }, mockTodoistApi);
|
|
170
|
+
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
171
|
+
parentId: 'parent-with-no-subtasks',
|
|
172
|
+
cursor: null,
|
|
173
|
+
limit: 10,
|
|
174
|
+
});
|
|
175
|
+
expect(result).toEqual({ tasks: [], nextCursor: null });
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('pagination and limits', () => {
|
|
179
|
+
it('should handle custom limit and cursor', async () => {
|
|
180
|
+
mockTodoistApi.getTasks.mockResolvedValue({ results: [], nextCursor: 'next-cursor' });
|
|
181
|
+
const result = await tasksListForContainer.execute({ type: 'section', id: 'test-section', limit: 25, cursor: 'current-cursor' }, mockTodoistApi);
|
|
182
|
+
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
183
|
+
limit: 25,
|
|
184
|
+
cursor: 'current-cursor',
|
|
185
|
+
sectionId: 'test-section',
|
|
186
|
+
});
|
|
187
|
+
expect(result.nextCursor).toBe('next-cursor');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('container type validation', () => {
|
|
191
|
+
it('should handle all container types correctly', async () => {
|
|
192
|
+
mockTodoistApi.getTasks.mockResolvedValue({ results: [], nextCursor: null });
|
|
193
|
+
// Test project container
|
|
194
|
+
await tasksListForContainer.execute({ type: 'project', id: 'proj-1', limit: 10 }, mockTodoistApi);
|
|
195
|
+
expect(mockTodoistApi.getTasks).toHaveBeenLastCalledWith(expect.objectContaining({ projectId: 'proj-1' }));
|
|
196
|
+
// Test section container
|
|
197
|
+
await tasksListForContainer.execute({ type: 'section', id: 'sect-1', limit: 10 }, mockTodoistApi);
|
|
198
|
+
expect(mockTodoistApi.getTasks).toHaveBeenLastCalledWith(expect.objectContaining({ sectionId: 'sect-1' }));
|
|
199
|
+
// Test parent container
|
|
200
|
+
await tasksListForContainer.execute({ type: 'parent', id: 'parent-1', limit: 10 }, mockTodoistApi);
|
|
201
|
+
expect(mockTodoistApi.getTasks).toHaveBeenLastCalledWith(expect.objectContaining({ parentId: 'parent-1' }));
|
|
202
|
+
expect(mockTodoistApi.getTasks).toHaveBeenCalledTimes(3);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe('error handling', () => {
|
|
206
|
+
it('should propagate API errors for project queries', async () => {
|
|
207
|
+
const apiError = new Error('API Error: Project not found');
|
|
208
|
+
mockTodoistApi.getTasks.mockRejectedValue(apiError);
|
|
209
|
+
await expect(tasksListForContainer.execute({ type: 'project', id: 'non-existent-project', limit: 10 }, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
|
|
210
|
+
});
|
|
211
|
+
it('should propagate API errors for section queries', async () => {
|
|
212
|
+
const apiError = new Error('API Error: Section not found');
|
|
213
|
+
mockTodoistApi.getTasks.mockRejectedValue(apiError);
|
|
214
|
+
await expect(tasksListForContainer.execute({ type: 'section', id: 'non-existent-section', limit: 10 }, mockTodoistApi)).rejects.toThrow('API Error: Section not found');
|
|
215
|
+
});
|
|
216
|
+
it('should propagate API errors for parent task queries', async () => {
|
|
217
|
+
const apiError = new Error('API Error: Parent task not found');
|
|
218
|
+
mockTodoistApi.getTasks.mockRejectedValue(apiError);
|
|
219
|
+
await expect(tasksListForContainer.execute({ type: 'parent', id: 'non-existent-parent', limit: 10 }, mockTodoistApi)).rejects.toThrow('API Error: Parent task not found');
|
|
220
|
+
});
|
|
221
|
+
it('should handle permission errors', async () => {
|
|
222
|
+
const permissionError = new Error('API Error: Insufficient permissions');
|
|
223
|
+
mockTodoistApi.getTasks.mockRejectedValue(permissionError);
|
|
224
|
+
await expect(tasksListForContainer.execute({ type: 'project', id: 'restricted-project', limit: 10 }, mockTodoistApi)).rejects.toThrow('API Error: Insufficient permissions');
|
|
225
|
+
});
|
|
226
|
+
it('should handle cursor validation errors', async () => {
|
|
227
|
+
const cursorError = new Error('API Error: Invalid cursor');
|
|
228
|
+
mockTodoistApi.getTasks.mockRejectedValue(cursorError);
|
|
229
|
+
await expect(tasksListForContainer.execute({ type: 'project', id: 'test-project', cursor: 'invalid-cursor', limit: 10 }, mockTodoistApi)).rejects.toThrow('API Error: Invalid cursor');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-organize-multiple.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-organize-multiple.test.ts"],"names":[],"mappings":""}
|