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