@doist/todoist-ai 4.16.0 → 4.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/filter-helpers.d.ts +1 -1
- package/dist/index.d.ts +1044 -196
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -81
- package/dist/main.js +15 -23
- package/dist/mcp-helpers.d.ts +5 -5
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-server-BADReNAy.js +3092 -0
- package/dist/todoist-tool.d.ts +9 -3
- package/dist/todoist-tool.d.ts.map +1 -1
- package/dist/tool-helpers.d.ts +1 -1
- package/dist/tools/add-comments.d.ts +69 -3
- package/dist/tools/add-comments.d.ts.map +1 -1
- package/dist/tools/add-projects.d.ts +34 -3
- package/dist/tools/add-projects.d.ts.map +1 -1
- package/dist/tools/add-sections.d.ts +14 -1
- package/dist/tools/add-sections.d.ts.map +1 -1
- package/dist/tools/add-tasks.d.ts +65 -10
- package/dist/tools/add-tasks.d.ts.map +1 -1
- package/dist/tools/complete-tasks.d.ts +20 -1
- package/dist/tools/complete-tasks.d.ts.map +1 -1
- package/dist/tools/delete-object.d.ts +16 -3
- package/dist/tools/delete-object.d.ts.map +1 -1
- package/dist/tools/fetch.d.ts +8 -1
- package/dist/tools/fetch.d.ts.map +1 -1
- package/dist/tools/find-activity.d.ts +44 -7
- package/dist/tools/find-activity.d.ts.map +1 -1
- package/dist/tools/find-comments.d.ts +69 -3
- package/dist/tools/find-comments.d.ts.map +1 -1
- package/dist/tools/find-completed-tasks.d.ts +63 -5
- package/dist/tools/find-completed-tasks.d.ts.map +1 -1
- package/dist/tools/find-project-collaborators.d.ts +33 -2
- package/dist/tools/find-project-collaborators.d.ts.map +1 -1
- package/dist/tools/find-projects.d.ts +35 -1
- package/dist/tools/find-projects.d.ts.map +1 -1
- package/dist/tools/find-sections.d.ts +15 -1
- package/dist/tools/find-sections.d.ts.map +1 -1
- package/dist/tools/find-tasks-by-date.d.ts +61 -3
- package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
- package/dist/tools/find-tasks.d.ts +63 -5
- package/dist/tools/find-tasks.d.ts.map +1 -1
- package/dist/tools/get-overview.d.ts +24 -1
- package/dist/tools/get-overview.d.ts.map +1 -1
- package/dist/tools/manage-assignments.d.ts +39 -2
- package/dist/tools/manage-assignments.d.ts.map +1 -1
- package/dist/tools/search.d.ts +17 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/update-comments.d.ts +76 -3
- package/dist/tools/update-comments.d.ts.map +1 -1
- package/dist/tools/update-projects.d.ts +43 -1
- package/dist/tools/update-projects.d.ts.map +1 -1
- package/dist/tools/update-sections.d.ts +17 -3
- package/dist/tools/update-sections.d.ts.map +1 -1
- package/dist/tools/update-tasks.d.ts +79 -13
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/tools/user-info.d.ts +19 -1
- package/dist/tools/user-info.d.ts.map +1 -1
- package/dist/utils/assignment-validator.d.ts +2 -2
- package/dist/utils/output-schemas.d.ts +233 -0
- package/dist/utils/output-schemas.d.ts.map +1 -0
- package/dist/utils/response-builders.d.ts +1 -3
- package/dist/utils/response-builders.d.ts.map +1 -1
- package/dist/utils/test-helpers.d.ts +1 -1
- package/dist/utils/user-resolver.d.ts +1 -1
- package/package.json +10 -8
- package/dist/filter-helpers.js +0 -79
- package/dist/mcp-helpers.js +0 -71
- package/dist/mcp-server.js +0 -142
- package/dist/todoist-tool.js +0 -1
- package/dist/tool-helpers.js +0 -125
- package/dist/tool-helpers.test.d.ts +0 -2
- package/dist/tool-helpers.test.d.ts.map +0 -1
- package/dist/tool-helpers.test.js +0 -223
- package/dist/tools/__tests__/add-comments.test.d.ts +0 -2
- package/dist/tools/__tests__/add-comments.test.d.ts.map +0 -1
- package/dist/tools/__tests__/add-comments.test.js +0 -241
- package/dist/tools/__tests__/add-projects.test.d.ts +0 -2
- package/dist/tools/__tests__/add-projects.test.d.ts.map +0 -1
- package/dist/tools/__tests__/add-projects.test.js +0 -174
- package/dist/tools/__tests__/add-sections.test.d.ts +0 -2
- package/dist/tools/__tests__/add-sections.test.d.ts.map +0 -1
- package/dist/tools/__tests__/add-sections.test.js +0 -185
- package/dist/tools/__tests__/add-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/add-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/add-tasks.test.js +0 -606
- package/dist/tools/__tests__/assignment-integration.test.d.ts +0 -2
- package/dist/tools/__tests__/assignment-integration.test.d.ts.map +0 -1
- package/dist/tools/__tests__/assignment-integration.test.js +0 -428
- package/dist/tools/__tests__/complete-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/complete-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/complete-tasks.test.js +0 -206
- package/dist/tools/__tests__/delete-object.test.d.ts +0 -2
- package/dist/tools/__tests__/delete-object.test.d.ts.map +0 -1
- package/dist/tools/__tests__/delete-object.test.js +0 -110
- package/dist/tools/__tests__/fetch.test.d.ts +0 -2
- package/dist/tools/__tests__/fetch.test.d.ts.map +0 -1
- package/dist/tools/__tests__/fetch.test.js +0 -279
- package/dist/tools/__tests__/find-activity.test.d.ts +0 -2
- package/dist/tools/__tests__/find-activity.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-activity.test.js +0 -229
- package/dist/tools/__tests__/find-comments.test.d.ts +0 -2
- package/dist/tools/__tests__/find-comments.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-comments.test.js +0 -236
- package/dist/tools/__tests__/find-completed-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/find-completed-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-completed-tasks.test.js +0 -423
- package/dist/tools/__tests__/find-projects.test.d.ts +0 -2
- package/dist/tools/__tests__/find-projects.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-projects.test.js +0 -154
- package/dist/tools/__tests__/find-sections.test.d.ts +0 -2
- package/dist/tools/__tests__/find-sections.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-sections.test.js +0 -313
- package/dist/tools/__tests__/find-tasks-by-date.test.d.ts +0 -2
- package/dist/tools/__tests__/find-tasks-by-date.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-tasks-by-date.test.js +0 -528
- package/dist/tools/__tests__/find-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/find-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/find-tasks.test.js +0 -771
- package/dist/tools/__tests__/get-overview.test.d.ts +0 -2
- package/dist/tools/__tests__/get-overview.test.d.ts.map +0 -1
- package/dist/tools/__tests__/get-overview.test.js +0 -225
- package/dist/tools/__tests__/search.test.d.ts +0 -2
- package/dist/tools/__tests__/search.test.d.ts.map +0 -1
- package/dist/tools/__tests__/search.test.js +0 -206
- package/dist/tools/__tests__/update-comments.test.d.ts +0 -2
- package/dist/tools/__tests__/update-comments.test.d.ts.map +0 -1
- package/dist/tools/__tests__/update-comments.test.js +0 -294
- package/dist/tools/__tests__/update-projects.test.d.ts +0 -2
- package/dist/tools/__tests__/update-projects.test.d.ts.map +0 -1
- package/dist/tools/__tests__/update-projects.test.js +0 -217
- package/dist/tools/__tests__/update-sections.test.d.ts +0 -2
- package/dist/tools/__tests__/update-sections.test.d.ts.map +0 -1
- package/dist/tools/__tests__/update-sections.test.js +0 -169
- package/dist/tools/__tests__/update-tasks.test.d.ts +0 -2
- package/dist/tools/__tests__/update-tasks.test.d.ts.map +0 -1
- package/dist/tools/__tests__/update-tasks.test.js +0 -788
- package/dist/tools/__tests__/user-info.test.d.ts +0 -2
- package/dist/tools/__tests__/user-info.test.d.ts.map +0 -1
- package/dist/tools/__tests__/user-info.test.js +0 -139
- package/dist/tools/add-comments.js +0 -89
- package/dist/tools/add-projects.js +0 -63
- package/dist/tools/add-sections.js +0 -74
- package/dist/tools/add-tasks.js +0 -169
- package/dist/tools/complete-tasks.js +0 -68
- package/dist/tools/delete-object.js +0 -79
- package/dist/tools/fetch.js +0 -102
- package/dist/tools/find-activity.js +0 -221
- package/dist/tools/find-comments.js +0 -148
- package/dist/tools/find-completed-tasks.js +0 -168
- package/dist/tools/find-project-collaborators.js +0 -151
- package/dist/tools/find-projects.js +0 -101
- package/dist/tools/find-sections.js +0 -101
- package/dist/tools/find-tasks-by-date.js +0 -198
- package/dist/tools/find-tasks.js +0 -329
- package/dist/tools/get-overview.js +0 -249
- package/dist/tools/manage-assignments.js +0 -337
- package/dist/tools/search.js +0 -65
- package/dist/tools/update-comments.js +0 -82
- package/dist/tools/update-projects.js +0 -84
- package/dist/tools/update-sections.js +0 -70
- package/dist/tools/update-tasks.js +0 -179
- package/dist/tools/user-info.js +0 -142
- package/dist/utils/assignment-validator.js +0 -253
- package/dist/utils/constants.js +0 -45
- package/dist/utils/duration-parser.js +0 -96
- package/dist/utils/duration-parser.test.d.ts +0 -2
- package/dist/utils/duration-parser.test.d.ts.map +0 -1
- package/dist/utils/duration-parser.test.js +0 -147
- package/dist/utils/labels.js +0 -18
- package/dist/utils/priorities.js +0 -20
- package/dist/utils/response-builders.js +0 -210
- package/dist/utils/sanitize-data.js +0 -37
- package/dist/utils/sanitize-data.test.d.ts +0 -2
- package/dist/utils/sanitize-data.test.d.ts.map +0 -1
- package/dist/utils/sanitize-data.test.js +0 -93
- package/dist/utils/test-helpers.js +0 -237
- package/dist/utils/tool-names.js +0 -40
- package/dist/utils/user-resolver.js +0 -179
|
@@ -1,771 +0,0 @@
|
|
|
1
|
-
import { jest } from '@jest/globals';
|
|
2
|
-
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
-
import { createMappedTask, createMockApiResponse, createMockTask, createMockUser, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, TODAY, } from '../../utils/test-helpers.js';
|
|
4
|
-
import { ToolNames } from '../../utils/tool-names.js';
|
|
5
|
-
import { resolveUserNameToId } from '../../utils/user-resolver.js';
|
|
6
|
-
import { findTasks } from '../find-tasks.js';
|
|
7
|
-
jest.mock('../../tool-helpers', () => {
|
|
8
|
-
const actual = jest.requireActual('../../tool-helpers');
|
|
9
|
-
return {
|
|
10
|
-
getTasksByFilter: jest.fn(),
|
|
11
|
-
mapTask: actual.mapTask,
|
|
12
|
-
filterTasksByResponsibleUser: actual.filterTasksByResponsibleUser,
|
|
13
|
-
};
|
|
14
|
-
});
|
|
15
|
-
jest.mock('../../utils/user-resolver', () => ({
|
|
16
|
-
resolveUserNameToId: jest.fn(),
|
|
17
|
-
}));
|
|
18
|
-
const { FIND_TASKS, UPDATE_TASKS, FIND_COMPLETED_TASKS } = ToolNames;
|
|
19
|
-
const mockGetTasksByFilter = getTasksByFilter;
|
|
20
|
-
const mockResolveUserNameToId = resolveUserNameToId;
|
|
21
|
-
// Mock the Todoist API
|
|
22
|
-
const mockTodoistApi = {
|
|
23
|
-
getTasks: jest.fn(),
|
|
24
|
-
getUser: jest.fn(),
|
|
25
|
-
};
|
|
26
|
-
// Mock the Todoist User
|
|
27
|
-
const mockTodoistUser = createMockUser();
|
|
28
|
-
describe(`${FIND_TASKS} tool`, () => {
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
mockTodoistApi.getUser.mockResolvedValue(mockTodoistUser);
|
|
31
|
-
jest.clearAllMocks();
|
|
32
|
-
});
|
|
33
|
-
describe('searching tasks', () => {
|
|
34
|
-
it('should search tasks and return results', async () => {
|
|
35
|
-
const mockTasks = [
|
|
36
|
-
createMappedTask({
|
|
37
|
-
id: TEST_IDS.TASK_1,
|
|
38
|
-
content: 'Task containing search term',
|
|
39
|
-
description: 'Description with more details',
|
|
40
|
-
labels: ['work'],
|
|
41
|
-
}),
|
|
42
|
-
createMappedTask({
|
|
43
|
-
id: TEST_IDS.TASK_2,
|
|
44
|
-
content: 'Another matching task',
|
|
45
|
-
priority: 2,
|
|
46
|
-
sectionId: TEST_IDS.SECTION_1,
|
|
47
|
-
}),
|
|
48
|
-
];
|
|
49
|
-
const mockResponse = { tasks: mockTasks, nextCursor: 'cursor-for-next-page' };
|
|
50
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
51
|
-
const result = await findTasks.execute({
|
|
52
|
-
searchText: 'important meeting',
|
|
53
|
-
limit: 10,
|
|
54
|
-
}, mockTodoistApi);
|
|
55
|
-
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
56
|
-
client: mockTodoistApi,
|
|
57
|
-
query: 'search: important meeting',
|
|
58
|
-
cursor: undefined,
|
|
59
|
-
limit: 10,
|
|
60
|
-
});
|
|
61
|
-
// Verify result is a concise summary
|
|
62
|
-
expect(extractTextContent(result)).toMatchSnapshot();
|
|
63
|
-
// Verify structured content
|
|
64
|
-
const structuredContent = extractStructuredContent(result);
|
|
65
|
-
expect(structuredContent).toEqual(expect.objectContaining({
|
|
66
|
-
tasks: expect.any(Array),
|
|
67
|
-
totalCount: 2,
|
|
68
|
-
hasMore: true,
|
|
69
|
-
nextCursor: 'cursor-for-next-page',
|
|
70
|
-
appliedFilters: {
|
|
71
|
-
searchText: 'important meeting',
|
|
72
|
-
limit: 10,
|
|
73
|
-
},
|
|
74
|
-
}));
|
|
75
|
-
expect(structuredContent).toEqual(expect.objectContaining({
|
|
76
|
-
tasks: expect.any(Array),
|
|
77
|
-
}));
|
|
78
|
-
});
|
|
79
|
-
it.each([
|
|
80
|
-
{
|
|
81
|
-
name: 'custom limit',
|
|
82
|
-
params: {
|
|
83
|
-
searchText: 'project update',
|
|
84
|
-
limit: 5,
|
|
85
|
-
},
|
|
86
|
-
expectedQuery: 'search: project update',
|
|
87
|
-
expectedLimit: 5,
|
|
88
|
-
expectedCursor: undefined,
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: 'pagination cursor',
|
|
92
|
-
params: {
|
|
93
|
-
searchText: 'follow up',
|
|
94
|
-
limit: 20,
|
|
95
|
-
cursor: 'cursor-from-first-page',
|
|
96
|
-
},
|
|
97
|
-
expectedQuery: 'search: follow up',
|
|
98
|
-
expectedLimit: 20,
|
|
99
|
-
expectedCursor: 'cursor-from-first-page',
|
|
100
|
-
},
|
|
101
|
-
])('should handle $name', async ({ params, expectedQuery, expectedLimit, expectedCursor }) => {
|
|
102
|
-
const mockTask = createMappedTask({ content: 'Test result' });
|
|
103
|
-
const mockResponse = { tasks: [mockTask], nextCursor: null };
|
|
104
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
105
|
-
const result = await findTasks.execute(params, mockTodoistApi);
|
|
106
|
-
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
107
|
-
client: mockTodoistApi,
|
|
108
|
-
query: expectedQuery,
|
|
109
|
-
cursor: expectedCursor,
|
|
110
|
-
limit: expectedLimit,
|
|
111
|
-
});
|
|
112
|
-
// Verify result is a concise summary
|
|
113
|
-
expect(extractTextContent(result)).toMatchSnapshot();
|
|
114
|
-
// Verify structured content
|
|
115
|
-
const structuredContent = extractStructuredContent(result);
|
|
116
|
-
expect(structuredContent).toEqual(expect.objectContaining({
|
|
117
|
-
tasks: expect.any(Array),
|
|
118
|
-
totalCount: 1,
|
|
119
|
-
hasMore: false,
|
|
120
|
-
appliedFilters: expect.objectContaining({
|
|
121
|
-
searchText: params.searchText,
|
|
122
|
-
limit: expectedLimit,
|
|
123
|
-
}),
|
|
124
|
-
}));
|
|
125
|
-
expect(structuredContent).toEqual(expect.objectContaining({
|
|
126
|
-
tasks: expect.any(Array),
|
|
127
|
-
}));
|
|
128
|
-
});
|
|
129
|
-
it.each([
|
|
130
|
-
{ searchText: '@work #urgent "exact phrase"', description: 'special characters' },
|
|
131
|
-
{ searchText: 'nonexistent keyword', description: 'empty results' },
|
|
132
|
-
])('should handle search with $description', async ({ searchText }) => {
|
|
133
|
-
const mockResponse = { tasks: [], nextCursor: null };
|
|
134
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
135
|
-
const result = await findTasks.execute({ searchText, limit: 10 }, mockTodoistApi);
|
|
136
|
-
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
137
|
-
client: mockTodoistApi,
|
|
138
|
-
query: `search: ${searchText}`,
|
|
139
|
-
cursor: undefined,
|
|
140
|
-
limit: 10,
|
|
141
|
-
});
|
|
142
|
-
// Verify result is a concise summary
|
|
143
|
-
expect(extractTextContent(result)).toMatchSnapshot();
|
|
144
|
-
// Verify structured content for empty results
|
|
145
|
-
const structuredContent = extractStructuredContent(result);
|
|
146
|
-
expect(structuredContent).toEqual({
|
|
147
|
-
// tasks array is removed when empty
|
|
148
|
-
totalCount: 0,
|
|
149
|
-
hasMore: false,
|
|
150
|
-
appliedFilters: expect.objectContaining({
|
|
151
|
-
searchText: searchText,
|
|
152
|
-
}),
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
describe('validation', () => {
|
|
157
|
-
it('should require at least one filter parameter', async () => {
|
|
158
|
-
await expect(findTasks.execute({ limit: 10 }, mockTodoistApi)).rejects.toThrow('At least one filter must be provided: searchText, projectId, sectionId, parentId, responsibleUser, or labels');
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
describe('container filtering', () => {
|
|
162
|
-
it.each([
|
|
163
|
-
{
|
|
164
|
-
name: 'project',
|
|
165
|
-
params: {
|
|
166
|
-
projectId: TEST_IDS.PROJECT_TEST,
|
|
167
|
-
limit: 10,
|
|
168
|
-
},
|
|
169
|
-
expectedApiParam: { projectId: TEST_IDS.PROJECT_TEST },
|
|
170
|
-
tasks: [createMockTask({ content: 'Project task' })],
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
name: 'section',
|
|
174
|
-
params: {
|
|
175
|
-
sectionId: TEST_IDS.SECTION_1,
|
|
176
|
-
limit: 10,
|
|
177
|
-
},
|
|
178
|
-
expectedApiParam: { sectionId: TEST_IDS.SECTION_1 },
|
|
179
|
-
tasks: [createMockTask({ content: 'Section task' })],
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
name: 'parent task',
|
|
183
|
-
params: {
|
|
184
|
-
parentId: TEST_IDS.TASK_1,
|
|
185
|
-
limit: 10,
|
|
186
|
-
},
|
|
187
|
-
expectedApiParam: { parentId: TEST_IDS.TASK_1 },
|
|
188
|
-
tasks: [createMockTask({ content: 'Subtask' })],
|
|
189
|
-
},
|
|
190
|
-
])('should find tasks in $name', async ({ params, expectedApiParam, tasks }) => {
|
|
191
|
-
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(tasks));
|
|
192
|
-
const result = await findTasks.execute(params, mockTodoistApi);
|
|
193
|
-
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
194
|
-
limit: 10,
|
|
195
|
-
cursor: null,
|
|
196
|
-
...expectedApiParam,
|
|
197
|
-
});
|
|
198
|
-
expect(extractTextContent(result)).toMatchSnapshot();
|
|
199
|
-
const structuredContent = extractStructuredContent(result);
|
|
200
|
-
expect(structuredContent).toEqual(expect.objectContaining({
|
|
201
|
-
tasks: expect.any(Array),
|
|
202
|
-
totalCount: tasks.length,
|
|
203
|
-
hasMore: false,
|
|
204
|
-
appliedFilters: params,
|
|
205
|
-
}));
|
|
206
|
-
});
|
|
207
|
-
it('should handle combined search text and container filtering', async () => {
|
|
208
|
-
const tasks = [
|
|
209
|
-
createMockTask({
|
|
210
|
-
id: '8485093749',
|
|
211
|
-
content: 'relevant task',
|
|
212
|
-
description: 'contains search term',
|
|
213
|
-
}),
|
|
214
|
-
createMockTask({
|
|
215
|
-
id: '8485093750',
|
|
216
|
-
content: 'other task',
|
|
217
|
-
description: 'different content',
|
|
218
|
-
}),
|
|
219
|
-
];
|
|
220
|
-
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(tasks));
|
|
221
|
-
const result = await findTasks.execute({
|
|
222
|
-
projectId: TEST_IDS.PROJECT_TEST,
|
|
223
|
-
searchText: 'relevant',
|
|
224
|
-
limit: 10,
|
|
225
|
-
}, mockTodoistApi);
|
|
226
|
-
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
227
|
-
limit: 10,
|
|
228
|
-
cursor: null,
|
|
229
|
-
projectId: TEST_IDS.PROJECT_TEST,
|
|
230
|
-
});
|
|
231
|
-
const structuredContent = extractStructuredContent(result);
|
|
232
|
-
expect(structuredContent.tasks).toHaveLength(1);
|
|
233
|
-
expect(structuredContent.tasks).toEqual([
|
|
234
|
-
expect.objectContaining({ content: 'relevant task' }),
|
|
235
|
-
]);
|
|
236
|
-
});
|
|
237
|
-
it('should handle empty containers', async () => {
|
|
238
|
-
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse([]));
|
|
239
|
-
const result = await findTasks.execute({
|
|
240
|
-
sectionId: 'empty-section',
|
|
241
|
-
limit: 10,
|
|
242
|
-
}, mockTodoistApi);
|
|
243
|
-
const textContent = extractTextContent(result);
|
|
244
|
-
expect(textContent).toContain('Section is empty');
|
|
245
|
-
expect(textContent).toContain('Tasks may be in other sections of the project');
|
|
246
|
-
});
|
|
247
|
-
it('should handle pagination with containers', async () => {
|
|
248
|
-
mockTodoistApi.getTasks.mockResolvedValue({
|
|
249
|
-
results: [],
|
|
250
|
-
nextCursor: 'next-cursor',
|
|
251
|
-
});
|
|
252
|
-
const result = await findTasks.execute({
|
|
253
|
-
projectId: TEST_IDS.PROJECT_TEST,
|
|
254
|
-
limit: 25,
|
|
255
|
-
cursor: 'current-cursor',
|
|
256
|
-
}, mockTodoistApi);
|
|
257
|
-
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
258
|
-
limit: 25,
|
|
259
|
-
cursor: 'current-cursor',
|
|
260
|
-
projectId: TEST_IDS.PROJECT_TEST,
|
|
261
|
-
});
|
|
262
|
-
const structuredContent = extractStructuredContent(result);
|
|
263
|
-
expect(structuredContent.hasMore).toBe(true);
|
|
264
|
-
expect(structuredContent.nextCursor).toBe('next-cursor');
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
describe('container error handling', () => {
|
|
268
|
-
it('should propagate API errors for container queries', async () => {
|
|
269
|
-
const apiError = new Error('API Error: Project not found');
|
|
270
|
-
mockTodoistApi.getTasks.mockRejectedValue(apiError);
|
|
271
|
-
await expect(findTasks.execute({ projectId: 'non-existent', limit: 10 }, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
describe('next steps logic', () => {
|
|
275
|
-
it('should suggest different actions when hasOverdue is true', async () => {
|
|
276
|
-
const mockTasks = [
|
|
277
|
-
createMappedTask({
|
|
278
|
-
id: TEST_IDS.TASK_1,
|
|
279
|
-
content: 'Overdue search result',
|
|
280
|
-
dueDate: '2025-08-10', // Past date
|
|
281
|
-
}),
|
|
282
|
-
];
|
|
283
|
-
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
284
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
285
|
-
const result = await findTasks.execute({ searchText: 'overdue tasks', limit: 10 }, mockTodoistApi);
|
|
286
|
-
const textContent = extractTextContent(result);
|
|
287
|
-
expect(textContent).toMatchSnapshot();
|
|
288
|
-
// Should prioritize overdue tasks in next steps
|
|
289
|
-
expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
|
|
290
|
-
});
|
|
291
|
-
it('should suggest today tasks when hasToday is true', async () => {
|
|
292
|
-
const mockTasks = [
|
|
293
|
-
createMappedTask({
|
|
294
|
-
id: TEST_IDS.TASK_1,
|
|
295
|
-
content: 'Task due today',
|
|
296
|
-
dueDate: TODAY,
|
|
297
|
-
}),
|
|
298
|
-
];
|
|
299
|
-
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
300
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
301
|
-
const result = await findTasks.execute({ searchText: 'today tasks', limit: 10 }, mockTodoistApi);
|
|
302
|
-
const textContent = extractTextContent(result);
|
|
303
|
-
expect(textContent).toMatchSnapshot();
|
|
304
|
-
// Should suggest today-focused actions
|
|
305
|
-
expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
|
|
306
|
-
});
|
|
307
|
-
it('should provide different next steps for regular tasks', async () => {
|
|
308
|
-
const mockTasks = [
|
|
309
|
-
createMappedTask({
|
|
310
|
-
id: TEST_IDS.TASK_1,
|
|
311
|
-
content: 'Regular future task',
|
|
312
|
-
dueDate: '2025-08-25', // Future date
|
|
313
|
-
}),
|
|
314
|
-
];
|
|
315
|
-
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
316
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
317
|
-
const result = await findTasks.execute({ searchText: 'future tasks', limit: 10 }, mockTodoistApi);
|
|
318
|
-
const textContent = extractTextContent(result);
|
|
319
|
-
expect(textContent).toMatchSnapshot();
|
|
320
|
-
expect(textContent).toContain(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
|
|
321
|
-
});
|
|
322
|
-
it('should provide helpful suggestions for empty search results', async () => {
|
|
323
|
-
const mockResponse = { tasks: [], nextCursor: null };
|
|
324
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
325
|
-
const result = await findTasks.execute({ searchText: 'nonexistent', limit: 10 }, mockTodoistApi);
|
|
326
|
-
const textContent = extractTextContent(result);
|
|
327
|
-
expect(textContent).toMatchSnapshot();
|
|
328
|
-
expect(textContent).toContain('Try broader search terms');
|
|
329
|
-
expect(textContent).toContain(`Check completed tasks with ${FIND_COMPLETED_TASKS}`);
|
|
330
|
-
expect(textContent).toContain('Verify spelling and try partial words');
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
describe('label filtering', () => {
|
|
334
|
-
it.each([
|
|
335
|
-
{
|
|
336
|
-
name: 'text search with single label OR operator',
|
|
337
|
-
params: {
|
|
338
|
-
searchText: 'important meeting',
|
|
339
|
-
limit: 10,
|
|
340
|
-
labels: ['work'],
|
|
341
|
-
},
|
|
342
|
-
expectedQuery: 'search: important meeting & (@work)',
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
name: 'text search with multiple labels AND operator',
|
|
346
|
-
params: {
|
|
347
|
-
searchText: 'project update',
|
|
348
|
-
limit: 15,
|
|
349
|
-
labels: ['work', 'urgent'],
|
|
350
|
-
labelsOperator: 'and',
|
|
351
|
-
},
|
|
352
|
-
expectedQuery: 'search: project update & (@work & @urgent)',
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
name: 'text search with multiple labels OR operator',
|
|
356
|
-
params: {
|
|
357
|
-
searchText: 'follow up',
|
|
358
|
-
limit: 20,
|
|
359
|
-
labels: ['personal', 'shopping'],
|
|
360
|
-
},
|
|
361
|
-
expectedQuery: 'search: follow up & (@personal | @shopping)',
|
|
362
|
-
},
|
|
363
|
-
])('should filter tasks by labels in text search: $name', async ({ params, expectedQuery }) => {
|
|
364
|
-
const mockTasks = [
|
|
365
|
-
createMappedTask({
|
|
366
|
-
id: TEST_IDS.TASK_1,
|
|
367
|
-
content: 'Task with work label',
|
|
368
|
-
labels: ['work'],
|
|
369
|
-
}),
|
|
370
|
-
];
|
|
371
|
-
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
372
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
373
|
-
const result = await findTasks.execute(params, mockTodoistApi);
|
|
374
|
-
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
375
|
-
client: mockTodoistApi,
|
|
376
|
-
query: expectedQuery,
|
|
377
|
-
cursor: undefined,
|
|
378
|
-
limit: params.limit,
|
|
379
|
-
});
|
|
380
|
-
const structuredContent = extractStructuredContent(result);
|
|
381
|
-
expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
|
|
382
|
-
searchText: params.searchText,
|
|
383
|
-
labels: params.labels,
|
|
384
|
-
...(params.labelsOperator ? { labelsOperator: params.labelsOperator } : {}),
|
|
385
|
-
}));
|
|
386
|
-
});
|
|
387
|
-
it.each([
|
|
388
|
-
{
|
|
389
|
-
name: 'project filter with labels',
|
|
390
|
-
params: {
|
|
391
|
-
projectId: TEST_IDS.PROJECT_TEST,
|
|
392
|
-
limit: 10,
|
|
393
|
-
labels: ['important'],
|
|
394
|
-
},
|
|
395
|
-
expectedApiParam: { projectId: TEST_IDS.PROJECT_TEST },
|
|
396
|
-
},
|
|
397
|
-
{
|
|
398
|
-
name: 'section filter with multiple labels',
|
|
399
|
-
params: {
|
|
400
|
-
sectionId: TEST_IDS.SECTION_1,
|
|
401
|
-
limit: 10,
|
|
402
|
-
labels: ['work', 'urgent'],
|
|
403
|
-
labelsOperator: 'and',
|
|
404
|
-
},
|
|
405
|
-
expectedApiParam: { sectionId: TEST_IDS.SECTION_1 },
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
name: 'parent task filter with labels',
|
|
409
|
-
params: {
|
|
410
|
-
parentId: TEST_IDS.TASK_1,
|
|
411
|
-
limit: 10,
|
|
412
|
-
labels: ['personal'],
|
|
413
|
-
},
|
|
414
|
-
expectedApiParam: { parentId: TEST_IDS.TASK_1 },
|
|
415
|
-
},
|
|
416
|
-
])('should apply label filtering to container searches: $name', async ({ params, expectedApiParam }) => {
|
|
417
|
-
const allTasks = [
|
|
418
|
-
createMockTask({
|
|
419
|
-
id: '1',
|
|
420
|
-
content: 'Task with matching label',
|
|
421
|
-
labels: params.labels,
|
|
422
|
-
}),
|
|
423
|
-
createMockTask({
|
|
424
|
-
id: '2',
|
|
425
|
-
content: 'Task without matching label',
|
|
426
|
-
labels: ['other'],
|
|
427
|
-
}),
|
|
428
|
-
];
|
|
429
|
-
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(allTasks));
|
|
430
|
-
const result = await findTasks.execute(params, mockTodoistApi);
|
|
431
|
-
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
432
|
-
limit: 10,
|
|
433
|
-
cursor: null,
|
|
434
|
-
...expectedApiParam,
|
|
435
|
-
});
|
|
436
|
-
// Should filter results client-side based on labels
|
|
437
|
-
const structuredContent = extractStructuredContent(result);
|
|
438
|
-
if (params.labelsOperator === 'and') {
|
|
439
|
-
// AND operation: task must have all specified labels
|
|
440
|
-
expect(structuredContent.tasks).toEqual(expect.arrayContaining([
|
|
441
|
-
expect.objectContaining({
|
|
442
|
-
labels: expect.arrayContaining(params.labels),
|
|
443
|
-
}),
|
|
444
|
-
]));
|
|
445
|
-
}
|
|
446
|
-
else {
|
|
447
|
-
// OR operation: task must have at least one of the specified labels
|
|
448
|
-
expect(structuredContent.tasks.length).toBeGreaterThanOrEqual(0);
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
it('should handle empty labels array', async () => {
|
|
452
|
-
const params = {
|
|
453
|
-
searchText: 'test',
|
|
454
|
-
limit: 10,
|
|
455
|
-
};
|
|
456
|
-
const mockResponse = { tasks: [], nextCursor: null };
|
|
457
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
458
|
-
await findTasks.execute(params, mockTodoistApi);
|
|
459
|
-
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
460
|
-
client: mockTodoistApi,
|
|
461
|
-
query: 'search: test',
|
|
462
|
-
cursor: undefined,
|
|
463
|
-
limit: 10,
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
it('should combine search text, container, and label filters', async () => {
|
|
467
|
-
const params = {
|
|
468
|
-
projectId: TEST_IDS.PROJECT_TEST,
|
|
469
|
-
searchText: 'important',
|
|
470
|
-
limit: 10,
|
|
471
|
-
labels: ['urgent'],
|
|
472
|
-
};
|
|
473
|
-
const allTasks = [
|
|
474
|
-
createMockTask({
|
|
475
|
-
id: '1',
|
|
476
|
-
content: 'important task',
|
|
477
|
-
description: 'urgent work',
|
|
478
|
-
labels: ['urgent'],
|
|
479
|
-
}),
|
|
480
|
-
createMockTask({
|
|
481
|
-
id: '2',
|
|
482
|
-
content: 'other task',
|
|
483
|
-
description: 'not important',
|
|
484
|
-
labels: ['work'],
|
|
485
|
-
}),
|
|
486
|
-
];
|
|
487
|
-
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(allTasks));
|
|
488
|
-
const result = await findTasks.execute(params, mockTodoistApi);
|
|
489
|
-
// Should call API with container filter
|
|
490
|
-
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
491
|
-
limit: 10,
|
|
492
|
-
cursor: null,
|
|
493
|
-
projectId: TEST_IDS.PROJECT_TEST,
|
|
494
|
-
});
|
|
495
|
-
// Should filter results by search text AND labels
|
|
496
|
-
const structuredContent = extractStructuredContent(result);
|
|
497
|
-
expect(structuredContent.tasks).toEqual([
|
|
498
|
-
expect.objectContaining({
|
|
499
|
-
content: 'important task',
|
|
500
|
-
labels: expect.arrayContaining(['urgent']),
|
|
501
|
-
}),
|
|
502
|
-
]);
|
|
503
|
-
});
|
|
504
|
-
it('should handle labels-only filtering', async () => {
|
|
505
|
-
const params = {
|
|
506
|
-
limit: 10,
|
|
507
|
-
labels: ['work'],
|
|
508
|
-
};
|
|
509
|
-
const mockTasks = [
|
|
510
|
-
createMappedTask({
|
|
511
|
-
id: TEST_IDS.TASK_1,
|
|
512
|
-
content: 'Task with work label',
|
|
513
|
-
labels: ['work'],
|
|
514
|
-
}),
|
|
515
|
-
];
|
|
516
|
-
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
517
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
518
|
-
const result = await findTasks.execute(params, mockTodoistApi);
|
|
519
|
-
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
520
|
-
client: mockTodoistApi,
|
|
521
|
-
query: '(@work)',
|
|
522
|
-
cursor: undefined,
|
|
523
|
-
limit: 10,
|
|
524
|
-
});
|
|
525
|
-
const structuredContent = extractStructuredContent(result);
|
|
526
|
-
expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
|
|
527
|
-
labels: ['work'],
|
|
528
|
-
}));
|
|
529
|
-
});
|
|
530
|
-
it('should handle labels with @ symbol', async () => {
|
|
531
|
-
const params = {
|
|
532
|
-
limit: 10,
|
|
533
|
-
labels: ['@work', 'personal'], // Mix of with and without @
|
|
534
|
-
};
|
|
535
|
-
const mockTasks = [
|
|
536
|
-
createMappedTask({
|
|
537
|
-
id: TEST_IDS.TASK_1,
|
|
538
|
-
content: 'Task with work label',
|
|
539
|
-
labels: ['work', 'personal'],
|
|
540
|
-
}),
|
|
541
|
-
];
|
|
542
|
-
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
543
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
544
|
-
const result = await findTasks.execute(params, mockTodoistApi);
|
|
545
|
-
// Should handle both @work (already has @) and personal (needs @ added)
|
|
546
|
-
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
547
|
-
client: mockTodoistApi,
|
|
548
|
-
query: '(@work | @personal)',
|
|
549
|
-
cursor: undefined,
|
|
550
|
-
limit: 10,
|
|
551
|
-
});
|
|
552
|
-
const structuredContent = extractStructuredContent(result);
|
|
553
|
-
expect(structuredContent.appliedFilters).toEqual(expect.objectContaining({
|
|
554
|
-
labels: ['@work', 'personal'],
|
|
555
|
-
}));
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
describe('markdown content preservation', () => {
|
|
559
|
-
it('should preserve markdown links and formatting in task content and description', async () => {
|
|
560
|
-
const richMarkdownContent = 'Test **bold** task with [link](https://example.com)';
|
|
561
|
-
const richMarkdownDescription = `This is a **comprehensive test** of markdown syntax in Todoist task descriptions:
|
|
562
|
-
|
|
563
|
-
### Links
|
|
564
|
-
[Wikipedia - Test Link](https://en.wikipedia.org/wiki/Test)
|
|
565
|
-
[GitHub Repository](https://github.com/Doist/todoist-ai)
|
|
566
|
-
[Google Search](https://www.google.com)
|
|
567
|
-
|
|
568
|
-
### Text Formatting
|
|
569
|
-
**Bold text here**
|
|
570
|
-
*Italic text here*
|
|
571
|
-
***Bold and italic***
|
|
572
|
-
\`inline code\`
|
|
573
|
-
|
|
574
|
-
### Lists
|
|
575
|
-
- Bullet point 1
|
|
576
|
-
- Bullet point 2
|
|
577
|
-
- Nested item
|
|
578
|
-
|
|
579
|
-
1. Numbered item 1
|
|
580
|
-
2. Numbered item 2
|
|
581
|
-
|
|
582
|
-
End of test content.`;
|
|
583
|
-
const mockTasks = [
|
|
584
|
-
createMappedTask({
|
|
585
|
-
id: TEST_IDS.TASK_1,
|
|
586
|
-
content: richMarkdownContent,
|
|
587
|
-
description: richMarkdownDescription,
|
|
588
|
-
}),
|
|
589
|
-
];
|
|
590
|
-
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
591
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
592
|
-
const result = await findTasks.execute({ searchText: 'markdown test', limit: 10 }, mockTodoistApi);
|
|
593
|
-
const structuredContent = extractStructuredContent(result);
|
|
594
|
-
// Verify that markdown links and formatting are preserved exactly as provided
|
|
595
|
-
expect(structuredContent.tasks).toHaveLength(1);
|
|
596
|
-
expect(structuredContent.tasks).toEqual(expect.arrayContaining([
|
|
597
|
-
expect.objectContaining({
|
|
598
|
-
content: richMarkdownContent,
|
|
599
|
-
description: richMarkdownDescription,
|
|
600
|
-
}),
|
|
601
|
-
]));
|
|
602
|
-
});
|
|
603
|
-
it('should preserve markdown links in container-based searches', async () => {
|
|
604
|
-
const taskWithLinks = createMockTask({
|
|
605
|
-
id: TEST_IDS.TASK_1,
|
|
606
|
-
content: 'Task with [external link](https://todoist.com)',
|
|
607
|
-
description: 'See this [documentation](https://docs.example.com) for details.',
|
|
608
|
-
});
|
|
609
|
-
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse([taskWithLinks]));
|
|
610
|
-
const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_TEST, limit: 10 }, mockTodoistApi);
|
|
611
|
-
const structuredContent = extractStructuredContent(result);
|
|
612
|
-
// Verify URLs are preserved in container-based searches too
|
|
613
|
-
expect(structuredContent.tasks).toHaveLength(1);
|
|
614
|
-
expect(structuredContent.tasks).toEqual(expect.arrayContaining([
|
|
615
|
-
expect.objectContaining({
|
|
616
|
-
content: 'Task with [external link](https://todoist.com)',
|
|
617
|
-
description: 'See this [documentation](https://docs.example.com) for details.',
|
|
618
|
-
}),
|
|
619
|
-
]));
|
|
620
|
-
});
|
|
621
|
-
});
|
|
622
|
-
describe('responsible user filtering', () => {
|
|
623
|
-
describe('when no responsibleUser is provided', () => {
|
|
624
|
-
it('should filter text search results to show only unassigned tasks or tasks assigned to current user', async () => {
|
|
625
|
-
const mockTasks = [
|
|
626
|
-
createMappedTask({
|
|
627
|
-
id: TEST_IDS.TASK_1,
|
|
628
|
-
content: 'My task',
|
|
629
|
-
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
630
|
-
}),
|
|
631
|
-
createMappedTask({
|
|
632
|
-
id: TEST_IDS.TASK_2,
|
|
633
|
-
content: 'Unassigned task',
|
|
634
|
-
responsibleUid: null, // Unassigned
|
|
635
|
-
}),
|
|
636
|
-
createMappedTask({
|
|
637
|
-
id: TEST_IDS.TASK_3,
|
|
638
|
-
content: 'Someone else task',
|
|
639
|
-
responsibleUid: 'other-user-id', // Assigned to someone else
|
|
640
|
-
}),
|
|
641
|
-
];
|
|
642
|
-
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
643
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
644
|
-
const result = await findTasks.execute({ searchText: 'task', limit: 10 }, mockTodoistApi);
|
|
645
|
-
const structuredContent = extractStructuredContent(result);
|
|
646
|
-
// Should only return tasks 1 and 2, not task 3
|
|
647
|
-
expect(structuredContent.tasks).toHaveLength(2);
|
|
648
|
-
expect(structuredContent.tasks.map((t) => t.id)).toEqual([TEST_IDS.TASK_1, TEST_IDS.TASK_2]);
|
|
649
|
-
});
|
|
650
|
-
it('should filter container-based results to show only unassigned tasks or tasks assigned to current user', async () => {
|
|
651
|
-
const mockTasks = [
|
|
652
|
-
createMockTask({
|
|
653
|
-
id: TEST_IDS.TASK_1,
|
|
654
|
-
content: 'My project task',
|
|
655
|
-
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
656
|
-
}),
|
|
657
|
-
createMockTask({
|
|
658
|
-
id: TEST_IDS.TASK_2,
|
|
659
|
-
content: 'Unassigned project task',
|
|
660
|
-
responsibleUid: null, // Unassigned
|
|
661
|
-
}),
|
|
662
|
-
createMockTask({
|
|
663
|
-
id: TEST_IDS.TASK_3,
|
|
664
|
-
content: 'Someone else project task',
|
|
665
|
-
responsibleUid: 'other-user-id', // Assigned to someone else
|
|
666
|
-
}),
|
|
667
|
-
];
|
|
668
|
-
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(mockTasks));
|
|
669
|
-
const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_WORK, limit: 10 }, mockTodoistApi);
|
|
670
|
-
const structuredContent = extractStructuredContent(result);
|
|
671
|
-
// Should only return tasks 1 and 2, not task 3
|
|
672
|
-
expect(structuredContent.tasks).toHaveLength(2);
|
|
673
|
-
expect(structuredContent.tasks.map((t) => t.id)).toEqual([TEST_IDS.TASK_1, TEST_IDS.TASK_2]);
|
|
674
|
-
});
|
|
675
|
-
});
|
|
676
|
-
describe('when responsibleUser is provided', () => {
|
|
677
|
-
it('should filter text search results to show only tasks assigned to specified user', async () => {
|
|
678
|
-
const mockTasks = [
|
|
679
|
-
createMappedTask({
|
|
680
|
-
id: TEST_IDS.TASK_1,
|
|
681
|
-
content: 'Task for John',
|
|
682
|
-
responsibleUid: 'specific-user-id', // Assigned to specified user
|
|
683
|
-
}),
|
|
684
|
-
createMappedTask({
|
|
685
|
-
id: TEST_IDS.TASK_2,
|
|
686
|
-
content: 'My task',
|
|
687
|
-
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
688
|
-
}),
|
|
689
|
-
createMappedTask({
|
|
690
|
-
id: TEST_IDS.TASK_3,
|
|
691
|
-
content: 'Unassigned task',
|
|
692
|
-
responsibleUid: null, // Unassigned
|
|
693
|
-
}),
|
|
694
|
-
];
|
|
695
|
-
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
696
|
-
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
697
|
-
mockResolveUserNameToId.mockResolvedValue({
|
|
698
|
-
userId: 'specific-user-id',
|
|
699
|
-
displayName: 'John Doe',
|
|
700
|
-
email: 'john@example.com',
|
|
701
|
-
});
|
|
702
|
-
const result = await findTasks.execute({ searchText: 'task', responsibleUser: 'John Doe', limit: 10 }, mockTodoistApi);
|
|
703
|
-
const structuredContent = extractStructuredContent(result);
|
|
704
|
-
// Should only return task 1 (assigned to John)
|
|
705
|
-
expect(structuredContent.tasks).toHaveLength(1);
|
|
706
|
-
expect(structuredContent.tasks[0]?.id).toBe(TEST_IDS.TASK_1);
|
|
707
|
-
});
|
|
708
|
-
it('should filter container-based results to show only tasks assigned to specified user', async () => {
|
|
709
|
-
const mockTasks = [
|
|
710
|
-
createMockTask({
|
|
711
|
-
id: TEST_IDS.TASK_1,
|
|
712
|
-
content: 'Task for John',
|
|
713
|
-
responsibleUid: 'specific-user-id', // Assigned to specified user
|
|
714
|
-
}),
|
|
715
|
-
createMockTask({
|
|
716
|
-
id: TEST_IDS.TASK_2,
|
|
717
|
-
content: 'My task',
|
|
718
|
-
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
719
|
-
}),
|
|
720
|
-
createMockTask({
|
|
721
|
-
id: TEST_IDS.TASK_3,
|
|
722
|
-
content: 'Unassigned task',
|
|
723
|
-
responsibleUid: null, // Unassigned
|
|
724
|
-
}),
|
|
725
|
-
];
|
|
726
|
-
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(mockTasks));
|
|
727
|
-
mockResolveUserNameToId.mockResolvedValue({
|
|
728
|
-
userId: 'specific-user-id',
|
|
729
|
-
displayName: 'John Doe',
|
|
730
|
-
email: 'john@example.com',
|
|
731
|
-
});
|
|
732
|
-
const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_WORK, responsibleUser: 'John Doe', limit: 10 }, mockTodoistApi);
|
|
733
|
-
const structuredContent = extractStructuredContent(result);
|
|
734
|
-
// Should only return task 1 (assigned to John)
|
|
735
|
-
expect(structuredContent.tasks).toHaveLength(1);
|
|
736
|
-
expect(structuredContent.tasks[0]?.id).toBe(TEST_IDS.TASK_1);
|
|
737
|
-
});
|
|
738
|
-
});
|
|
739
|
-
});
|
|
740
|
-
describe('error handling', () => {
|
|
741
|
-
it.each([
|
|
742
|
-
{
|
|
743
|
-
error: 'At least one filter must be provided: searchText, projectId, sectionId, parentId, responsibleUser, or labels',
|
|
744
|
-
params: { limit: 10 },
|
|
745
|
-
expectValidation: true,
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
error: TEST_ERRORS.API_RATE_LIMIT,
|
|
749
|
-
params: {
|
|
750
|
-
searchText: 'any search term',
|
|
751
|
-
limit: 10,
|
|
752
|
-
},
|
|
753
|
-
expectValidation: false,
|
|
754
|
-
},
|
|
755
|
-
{
|
|
756
|
-
error: TEST_ERRORS.INVALID_CURSOR,
|
|
757
|
-
params: {
|
|
758
|
-
searchText: 'test',
|
|
759
|
-
cursor: 'invalid-cursor-format',
|
|
760
|
-
limit: 10,
|
|
761
|
-
},
|
|
762
|
-
expectValidation: false,
|
|
763
|
-
},
|
|
764
|
-
])('should propagate $error', async ({ error, params, expectValidation }) => {
|
|
765
|
-
if (!expectValidation) {
|
|
766
|
-
mockGetTasksByFilter.mockRejectedValue(new Error(error));
|
|
767
|
-
}
|
|
768
|
-
await expect(findTasks.execute(params, mockTodoistApi)).rejects.toThrow(error);
|
|
769
|
-
});
|
|
770
|
-
});
|
|
771
|
-
});
|