@doist/todoist-ai 2.0.0 → 2.1.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 +7 -0
- package/dist/index.d.ts +29 -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 +13 -1
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +43 -22
- package/dist/tool-helpers.test.js +55 -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 +274 -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 +251 -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.d.ts +5 -0
- package/dist/tools/tasks-add-multiple.d.ts.map +1 -1
- package/dist/tools/tasks-add-multiple.js +37 -17
- package/dist/tools/tasks-complete-multiple.js +3 -6
- package/dist/tools/tasks-list-by-date.d.ts +1 -0
- package/dist/tools/tasks-list-by-date.d.ts.map +1 -1
- package/dist/tools/tasks-list-by-date.js +12 -15
- package/dist/tools/tasks-list-completed.d.ts +2 -1
- package/dist/tools/tasks-list-completed.d.ts.map +1 -1
- package/dist/tools/tasks-list-completed.js +13 -16
- package/dist/tools/tasks-list-for-container.d.ts +1 -0
- package/dist/tools/tasks-list-for-container.d.ts.map +1 -1
- 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.d.ts +1 -0
- package/dist/tools/tasks-search.d.ts.map +1 -1
- package/dist/tools/tasks-search.js +7 -10
- package/dist/tools/tasks-update-one.d.ts +4 -2
- package/dist/tools/tasks-update-one.d.ts.map +1 -1
- package/dist/tools/tasks-update-one.js +45 -15
- package/dist/tools/test-helpers.d.ts +80 -0
- package/dist/tools/test-helpers.d.ts.map +1 -0
- package/dist/tools/test-helpers.js +140 -0
- package/dist/utils/duration-parser.d.ts +36 -0
- package/dist/utils/duration-parser.d.ts.map +1 -0
- package/dist/utils/duration-parser.js +96 -0
- package/dist/utils/duration-parser.test.d.ts +2 -0
- package/dist/utils/duration-parser.test.d.ts.map +1 -0
- package/dist/utils/duration-parser.test.js +147 -0
- package/package.json +6 -2
- package/scripts/test-executable.cjs +69 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tool_helpers_1 = require("./tool-helpers");
|
|
1
|
+
import { createMoveTaskArgs, isPersonalProject, isWorkspaceProject, mapProject, mapTask, } from './tool-helpers.js';
|
|
4
2
|
describe('shared utilities', () => {
|
|
5
3
|
describe('mapTask', () => {
|
|
6
4
|
it('should map a basic task correctly', () => {
|
|
@@ -21,8 +19,7 @@ describe('shared utilities', () => {
|
|
|
21
19
|
timezone: 'UTC',
|
|
22
20
|
},
|
|
23
21
|
};
|
|
24
|
-
|
|
25
|
-
expect(result).toEqual({
|
|
22
|
+
expect(mapTask(mockTask)).toEqual({
|
|
26
23
|
id: '123',
|
|
27
24
|
content: 'Test task',
|
|
28
25
|
description: 'Test description',
|
|
@@ -33,6 +30,7 @@ describe('shared utilities', () => {
|
|
|
33
30
|
sectionId: null,
|
|
34
31
|
parentId: null,
|
|
35
32
|
labels: ['work'],
|
|
33
|
+
duration: null,
|
|
36
34
|
});
|
|
37
35
|
});
|
|
38
36
|
it('should handle recurring tasks', () => {
|
|
@@ -53,8 +51,27 @@ describe('shared utilities', () => {
|
|
|
53
51
|
timezone: 'UTC',
|
|
54
52
|
},
|
|
55
53
|
};
|
|
56
|
-
const result =
|
|
54
|
+
const result = mapTask(mockTask);
|
|
57
55
|
expect(result.recurring).toBe('every day');
|
|
56
|
+
expect(result.duration).toBe(null);
|
|
57
|
+
});
|
|
58
|
+
it('should handle task with duration', () => {
|
|
59
|
+
const mockTask = {
|
|
60
|
+
id: '789',
|
|
61
|
+
content: 'Task with duration',
|
|
62
|
+
description: '',
|
|
63
|
+
projectId: 'proj-1',
|
|
64
|
+
sectionId: null,
|
|
65
|
+
parentId: null,
|
|
66
|
+
labels: [],
|
|
67
|
+
priority: 1,
|
|
68
|
+
duration: {
|
|
69
|
+
amount: 150,
|
|
70
|
+
unit: 'minute',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const result = mapTask(mockTask);
|
|
74
|
+
expect(result.duration).toBe('2h30m');
|
|
58
75
|
});
|
|
59
76
|
});
|
|
60
77
|
describe('mapProject', () => {
|
|
@@ -69,8 +86,7 @@ describe('shared utilities', () => {
|
|
|
69
86
|
inboxProject: false,
|
|
70
87
|
viewStyle: 'list',
|
|
71
88
|
};
|
|
72
|
-
|
|
73
|
-
expect(result).toEqual({
|
|
89
|
+
expect(mapProject(mockPersonalProject)).toEqual({
|
|
74
90
|
id: 'proj-1',
|
|
75
91
|
name: 'Personal Project',
|
|
76
92
|
color: 'blue',
|
|
@@ -90,8 +106,7 @@ describe('shared utilities', () => {
|
|
|
90
106
|
isShared: true,
|
|
91
107
|
viewStyle: 'board',
|
|
92
108
|
};
|
|
93
|
-
|
|
94
|
-
expect(result).toEqual({
|
|
109
|
+
expect(mapProject(mockWorkspaceProject)).toEqual({
|
|
95
110
|
id: 'proj-2',
|
|
96
111
|
name: 'Workspace Project',
|
|
97
112
|
color: 'red',
|
|
@@ -115,8 +130,8 @@ describe('shared utilities', () => {
|
|
|
115
130
|
inboxProject: true,
|
|
116
131
|
viewStyle: 'list',
|
|
117
132
|
};
|
|
118
|
-
expect(
|
|
119
|
-
expect(
|
|
133
|
+
expect(isPersonalProject(personalProject)).toBe(true);
|
|
134
|
+
expect(isWorkspaceProject(personalProject)).toBe(false);
|
|
120
135
|
});
|
|
121
136
|
it('should correctly identify workspace projects', () => {
|
|
122
137
|
const workspaceProject = {
|
|
@@ -128,8 +143,34 @@ describe('shared utilities', () => {
|
|
|
128
143
|
viewStyle: 'board',
|
|
129
144
|
accessLevel: 'admin',
|
|
130
145
|
};
|
|
131
|
-
expect(
|
|
132
|
-
expect(
|
|
146
|
+
expect(isWorkspaceProject(workspaceProject)).toBe(true);
|
|
147
|
+
expect(isPersonalProject(workspaceProject)).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('createMoveTaskArgs', () => {
|
|
151
|
+
it('should create MoveTaskArgs for projectId', () => {
|
|
152
|
+
const result = createMoveTaskArgs('task-1', 'project-123');
|
|
153
|
+
expect(result).toEqual({ projectId: 'project-123' });
|
|
154
|
+
});
|
|
155
|
+
it('should create MoveTaskArgs for sectionId', () => {
|
|
156
|
+
const result = createMoveTaskArgs('task-1', undefined, 'section-456');
|
|
157
|
+
expect(result).toEqual({ sectionId: 'section-456' });
|
|
158
|
+
});
|
|
159
|
+
it('should create MoveTaskArgs for parentId', () => {
|
|
160
|
+
const result = createMoveTaskArgs('task-1', undefined, undefined, 'parent-789');
|
|
161
|
+
expect(result).toEqual({ parentId: 'parent-789' });
|
|
162
|
+
});
|
|
163
|
+
it('should throw error when multiple move parameters are provided', () => {
|
|
164
|
+
expect(() => createMoveTaskArgs('task-1', 'project-123', 'section-456')).toThrow('Task task-1: Only one of projectId, sectionId, or parentId can be specified at a time');
|
|
165
|
+
});
|
|
166
|
+
it('should throw error when all three move parameters are provided', () => {
|
|
167
|
+
expect(() => createMoveTaskArgs('task-1', 'project-123', 'section-456', 'parent-789')).toThrow('Task task-1: Only one of projectId, sectionId, or parentId can be specified at a time');
|
|
168
|
+
});
|
|
169
|
+
it('should throw error when no move parameters are provided', () => {
|
|
170
|
+
expect(() => createMoveTaskArgs('task-1')).toThrow('Task task-1: At least one of projectId, sectionId, or parentId must be provided');
|
|
171
|
+
});
|
|
172
|
+
it('should throw error when empty strings are provided', () => {
|
|
173
|
+
expect(() => createMoveTaskArgs('task-1', '', '', '')).toThrow('Task task-1: At least one of projectId, sectionId, or parentId must be provided');
|
|
133
174
|
});
|
|
134
175
|
});
|
|
135
176
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-one.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/delete-one.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { deleteOne } from '../delete-one.js';
|
|
3
|
+
// Mock the Todoist API
|
|
4
|
+
const mockTodoistApi = {
|
|
5
|
+
deleteProject: jest.fn(),
|
|
6
|
+
deleteSection: jest.fn(),
|
|
7
|
+
deleteTask: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
describe('delete-one tool', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
describe('deleting projects', () => {
|
|
14
|
+
it('should delete a project by ID', async () => {
|
|
15
|
+
mockTodoistApi.deleteProject.mockResolvedValue(true);
|
|
16
|
+
const result = await deleteOne.execute({ type: 'project', id: '6cfCcrrCFg2xP94Q' }, mockTodoistApi);
|
|
17
|
+
// Verify API was called correctly
|
|
18
|
+
expect(mockTodoistApi.deleteProject).toHaveBeenCalledWith('6cfCcrrCFg2xP94Q');
|
|
19
|
+
expect(mockTodoistApi.deleteSection).not.toHaveBeenCalled();
|
|
20
|
+
expect(mockTodoistApi.deleteTask).not.toHaveBeenCalled();
|
|
21
|
+
// Verify success response
|
|
22
|
+
expect(result).toEqual({ success: true });
|
|
23
|
+
});
|
|
24
|
+
it('should propagate project deletion errors', async () => {
|
|
25
|
+
const apiError = new Error('API Error: Cannot delete project with tasks');
|
|
26
|
+
mockTodoistApi.deleteProject.mockRejectedValue(apiError);
|
|
27
|
+
await expect(deleteOne.execute({ type: 'project', id: 'project-with-tasks' }, mockTodoistApi)).rejects.toThrow('API Error: Cannot delete project with tasks');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('deleting sections', () => {
|
|
31
|
+
it('should delete a section by ID', async () => {
|
|
32
|
+
mockTodoistApi.deleteSection.mockResolvedValue(true);
|
|
33
|
+
const result = await deleteOne.execute({ type: 'section', id: 'section-123' }, mockTodoistApi);
|
|
34
|
+
// Verify API was called correctly
|
|
35
|
+
expect(mockTodoistApi.deleteSection).toHaveBeenCalledWith('section-123');
|
|
36
|
+
expect(mockTodoistApi.deleteProject).not.toHaveBeenCalled();
|
|
37
|
+
expect(mockTodoistApi.deleteTask).not.toHaveBeenCalled();
|
|
38
|
+
// Verify success response
|
|
39
|
+
expect(result).toEqual({ success: true });
|
|
40
|
+
});
|
|
41
|
+
it('should propagate section deletion errors', async () => {
|
|
42
|
+
const apiError = new Error('API Error: Section not found');
|
|
43
|
+
mockTodoistApi.deleteSection.mockRejectedValue(apiError);
|
|
44
|
+
await expect(deleteOne.execute({ type: 'section', id: 'non-existent-section' }, mockTodoistApi)).rejects.toThrow('API Error: Section not found');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('deleting tasks', () => {
|
|
48
|
+
it('should delete a task by ID', async () => {
|
|
49
|
+
mockTodoistApi.deleteTask.mockResolvedValue(true);
|
|
50
|
+
const result = await deleteOne.execute({ type: 'task', id: '8485093748' }, mockTodoistApi);
|
|
51
|
+
// Verify API was called correctly
|
|
52
|
+
expect(mockTodoistApi.deleteTask).toHaveBeenCalledWith('8485093748');
|
|
53
|
+
expect(mockTodoistApi.deleteProject).not.toHaveBeenCalled();
|
|
54
|
+
expect(mockTodoistApi.deleteSection).not.toHaveBeenCalled();
|
|
55
|
+
// Verify success response
|
|
56
|
+
expect(result).toEqual({ success: true });
|
|
57
|
+
});
|
|
58
|
+
it('should propagate task deletion errors', async () => {
|
|
59
|
+
const apiError = new Error('API Error: Task not found');
|
|
60
|
+
mockTodoistApi.deleteTask.mockRejectedValue(apiError);
|
|
61
|
+
await expect(deleteOne.execute({ type: 'task', id: 'non-existent-task' }, mockTodoistApi)).rejects.toThrow('API Error: Task not found');
|
|
62
|
+
});
|
|
63
|
+
it('should handle permission errors', async () => {
|
|
64
|
+
const apiError = new Error('API Error: Insufficient permissions to delete task');
|
|
65
|
+
mockTodoistApi.deleteTask.mockRejectedValue(apiError);
|
|
66
|
+
await expect(deleteOne.execute({ type: 'task', id: 'restricted-task' }, mockTodoistApi)).rejects.toThrow('API Error: Insufficient permissions to delete task');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('type validation', () => {
|
|
70
|
+
it('should handle all supported entity types', async () => {
|
|
71
|
+
// Test all three supported types work correctly
|
|
72
|
+
mockTodoistApi.deleteProject.mockResolvedValue(true);
|
|
73
|
+
mockTodoistApi.deleteSection.mockResolvedValue(true);
|
|
74
|
+
mockTodoistApi.deleteTask.mockResolvedValue(true);
|
|
75
|
+
// Delete project
|
|
76
|
+
await deleteOne.execute({ type: 'project', id: 'proj-1' }, mockTodoistApi);
|
|
77
|
+
expect(mockTodoistApi.deleteProject).toHaveBeenCalledWith('proj-1');
|
|
78
|
+
// Delete section
|
|
79
|
+
await deleteOne.execute({ type: 'section', id: 'sect-1' }, mockTodoistApi);
|
|
80
|
+
expect(mockTodoistApi.deleteSection).toHaveBeenCalledWith('sect-1');
|
|
81
|
+
// Delete task
|
|
82
|
+
await deleteOne.execute({ type: 'task', id: 'task-1' }, mockTodoistApi);
|
|
83
|
+
expect(mockTodoistApi.deleteTask).toHaveBeenCalledWith('task-1');
|
|
84
|
+
// Verify each API method was called exactly once
|
|
85
|
+
expect(mockTodoistApi.deleteProject).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(mockTodoistApi.deleteSection).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(mockTodoistApi.deleteTask).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overview.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/overview.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { overview } from '../overview.js';
|
|
3
|
+
import { TEST_ERRORS, TEST_IDS, createMockProject, createMockSection, createMockTask, } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
getProjects: jest.fn(),
|
|
7
|
+
getProject: jest.fn(),
|
|
8
|
+
getSections: jest.fn(),
|
|
9
|
+
getTasks: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
describe('overview tool', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
describe('account overview (no projectId)', () => {
|
|
16
|
+
it('should generate account overview with projects and sections', async () => {
|
|
17
|
+
const mockProjects = [
|
|
18
|
+
createMockProject({
|
|
19
|
+
id: TEST_IDS.PROJECT_INBOX,
|
|
20
|
+
name: 'Inbox',
|
|
21
|
+
color: 'grey',
|
|
22
|
+
inboxProject: true,
|
|
23
|
+
childOrder: 0,
|
|
24
|
+
}),
|
|
25
|
+
createMockProject({
|
|
26
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
27
|
+
name: 'test-abc123def456-project',
|
|
28
|
+
childOrder: 1,
|
|
29
|
+
}),
|
|
30
|
+
];
|
|
31
|
+
const mockSections = [
|
|
32
|
+
createMockSection({
|
|
33
|
+
id: TEST_IDS.SECTION_1,
|
|
34
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
35
|
+
name: 'test-section',
|
|
36
|
+
}),
|
|
37
|
+
];
|
|
38
|
+
mockTodoistApi.getProjects.mockResolvedValue({
|
|
39
|
+
results: mockProjects,
|
|
40
|
+
nextCursor: null,
|
|
41
|
+
});
|
|
42
|
+
mockTodoistApi.getSections.mockImplementation(({ projectId }) => {
|
|
43
|
+
if (projectId === TEST_IDS.PROJECT_TEST) {
|
|
44
|
+
return Promise.resolve({ results: mockSections, nextCursor: null });
|
|
45
|
+
}
|
|
46
|
+
return Promise.resolve({ results: [], nextCursor: null });
|
|
47
|
+
});
|
|
48
|
+
const result = await overview.execute({}, mockTodoistApi);
|
|
49
|
+
expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({});
|
|
50
|
+
expect(mockTodoistApi.getSections).toHaveBeenCalledTimes(2); // Once for each project
|
|
51
|
+
// Use snapshot testing for complex markdown output
|
|
52
|
+
expect(result).toMatchSnapshot();
|
|
53
|
+
});
|
|
54
|
+
it('should handle empty projects list', async () => {
|
|
55
|
+
mockTodoistApi.getProjects.mockResolvedValue({ results: [], nextCursor: null });
|
|
56
|
+
expect(await overview.execute({}, mockTodoistApi)).toMatchSnapshot();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('project overview (with projectId)', () => {
|
|
60
|
+
it('should generate detailed project overview with tasks', async () => {
|
|
61
|
+
const mockProject = createMockProject({
|
|
62
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
63
|
+
name: 'test-abc123def456-project',
|
|
64
|
+
});
|
|
65
|
+
const mockSections = [
|
|
66
|
+
createMockSection({
|
|
67
|
+
id: TEST_IDS.SECTION_1,
|
|
68
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
69
|
+
name: 'To Do',
|
|
70
|
+
}),
|
|
71
|
+
createMockSection({
|
|
72
|
+
id: TEST_IDS.SECTION_2,
|
|
73
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
74
|
+
sectionOrder: 2,
|
|
75
|
+
name: 'In Progress',
|
|
76
|
+
}),
|
|
77
|
+
];
|
|
78
|
+
const mockTasks = [
|
|
79
|
+
createMockTask({
|
|
80
|
+
id: TEST_IDS.TASK_1,
|
|
81
|
+
content: 'Task without section',
|
|
82
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
83
|
+
deadline: {
|
|
84
|
+
date: '2025-08-15',
|
|
85
|
+
lang: 'en',
|
|
86
|
+
},
|
|
87
|
+
responsibleUid: TEST_IDS.USER_ID,
|
|
88
|
+
assignedByUid: TEST_IDS.USER_ID,
|
|
89
|
+
}),
|
|
90
|
+
createMockTask({
|
|
91
|
+
id: TEST_IDS.TASK_2,
|
|
92
|
+
content: 'Task in To Do section',
|
|
93
|
+
description: 'Important task',
|
|
94
|
+
labels: ['work'],
|
|
95
|
+
priority: 2,
|
|
96
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
97
|
+
sectionId: TEST_IDS.SECTION_1,
|
|
98
|
+
}),
|
|
99
|
+
createMockTask({
|
|
100
|
+
id: TEST_IDS.TASK_3,
|
|
101
|
+
content: 'Subtask of important task',
|
|
102
|
+
childOrder: 2,
|
|
103
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
104
|
+
sectionId: TEST_IDS.SECTION_1,
|
|
105
|
+
parentId: TEST_IDS.TASK_2,
|
|
106
|
+
}),
|
|
107
|
+
];
|
|
108
|
+
mockTodoistApi.getProject.mockResolvedValue(mockProject);
|
|
109
|
+
mockTodoistApi.getSections.mockResolvedValue({
|
|
110
|
+
results: mockSections,
|
|
111
|
+
nextCursor: null,
|
|
112
|
+
});
|
|
113
|
+
mockTodoistApi.getTasks.mockResolvedValue({
|
|
114
|
+
results: mockTasks,
|
|
115
|
+
nextCursor: null,
|
|
116
|
+
});
|
|
117
|
+
const result = await overview.execute({ projectId: TEST_IDS.PROJECT_TEST }, mockTodoistApi);
|
|
118
|
+
expect(mockTodoistApi.getProject).toHaveBeenCalledWith(TEST_IDS.PROJECT_TEST);
|
|
119
|
+
expect(mockTodoistApi.getSections).toHaveBeenCalledWith({
|
|
120
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
121
|
+
});
|
|
122
|
+
expect(mockTodoistApi.getTasks).toHaveBeenCalledWith({
|
|
123
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
124
|
+
limit: 50,
|
|
125
|
+
cursor: undefined,
|
|
126
|
+
});
|
|
127
|
+
// Use snapshot testing for complex markdown output
|
|
128
|
+
expect(result).toMatchSnapshot();
|
|
129
|
+
});
|
|
130
|
+
it('should handle project with no tasks', async () => {
|
|
131
|
+
const mockProject = createMockProject({
|
|
132
|
+
id: 'empty-project-id',
|
|
133
|
+
name: 'Empty Project',
|
|
134
|
+
color: 'blue',
|
|
135
|
+
});
|
|
136
|
+
mockTodoistApi.getProject.mockResolvedValue(mockProject);
|
|
137
|
+
mockTodoistApi.getSections.mockResolvedValue({ results: [], nextCursor: null });
|
|
138
|
+
mockTodoistApi.getTasks.mockResolvedValue({ results: [], nextCursor: null });
|
|
139
|
+
const result = await overview.execute({ projectId: 'empty-project-id' }, mockTodoistApi);
|
|
140
|
+
expect(result).toMatchSnapshot();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('error handling', () => {
|
|
144
|
+
it.each([
|
|
145
|
+
{
|
|
146
|
+
scenario: 'project retrieval',
|
|
147
|
+
error: 'API Error: Project not found',
|
|
148
|
+
params: { projectId: 'non-existent-project' },
|
|
149
|
+
mockMethod: 'getProject',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
scenario: 'projects list',
|
|
153
|
+
error: TEST_ERRORS.API_UNAUTHORIZED,
|
|
154
|
+
params: {},
|
|
155
|
+
mockMethod: 'getProjects',
|
|
156
|
+
},
|
|
157
|
+
])('should propagate API errors for $scenario', async ({ error, params, mockMethod }) => {
|
|
158
|
+
const apiError = new Error(error);
|
|
159
|
+
mockTodoistApi[mockMethod].mockRejectedValue(apiError);
|
|
160
|
+
await expect(overview.execute(params, mockTodoistApi)).rejects.toThrow(error);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects-list.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/projects-list.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { projectsList } from '../projects-list.js';
|
|
3
|
+
import { TEST_ERRORS, TEST_IDS, createMockApiResponse, createMockProject } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
getProjects: jest.fn(),
|
|
7
|
+
};
|
|
8
|
+
describe('projects-list tool', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
describe('listing all projects', () => {
|
|
13
|
+
it('should list all projects when no search parameter is provided', async () => {
|
|
14
|
+
const mockProjects = [
|
|
15
|
+
createMockProject({
|
|
16
|
+
id: TEST_IDS.PROJECT_INBOX,
|
|
17
|
+
name: 'Inbox',
|
|
18
|
+
color: 'grey',
|
|
19
|
+
inboxProject: true,
|
|
20
|
+
childOrder: 0,
|
|
21
|
+
}),
|
|
22
|
+
createMockProject({
|
|
23
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
24
|
+
name: 'test-abc123def456-project',
|
|
25
|
+
color: 'charcoal',
|
|
26
|
+
childOrder: 1,
|
|
27
|
+
}),
|
|
28
|
+
createMockProject({
|
|
29
|
+
id: TEST_IDS.PROJECT_WORK,
|
|
30
|
+
name: 'Work Project',
|
|
31
|
+
color: 'blue',
|
|
32
|
+
isFavorite: true,
|
|
33
|
+
isShared: true,
|
|
34
|
+
viewStyle: 'board',
|
|
35
|
+
childOrder: 2,
|
|
36
|
+
description: 'Important work tasks',
|
|
37
|
+
canAssignTasks: true,
|
|
38
|
+
}),
|
|
39
|
+
];
|
|
40
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
|
|
41
|
+
const result = await projectsList.execute({ limit: 50 }, mockTodoistApi);
|
|
42
|
+
// Verify API was called correctly
|
|
43
|
+
expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({
|
|
44
|
+
limit: 50,
|
|
45
|
+
cursor: null,
|
|
46
|
+
});
|
|
47
|
+
// Verify result is properly mapped
|
|
48
|
+
expect(result).toEqual({
|
|
49
|
+
projects: [
|
|
50
|
+
expect.objectContaining({
|
|
51
|
+
id: TEST_IDS.PROJECT_INBOX,
|
|
52
|
+
name: 'Inbox',
|
|
53
|
+
color: 'grey',
|
|
54
|
+
inboxProject: true,
|
|
55
|
+
}),
|
|
56
|
+
expect.objectContaining({
|
|
57
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
58
|
+
name: 'test-abc123def456-project',
|
|
59
|
+
color: 'charcoal',
|
|
60
|
+
}),
|
|
61
|
+
expect.objectContaining({
|
|
62
|
+
id: TEST_IDS.PROJECT_WORK,
|
|
63
|
+
name: 'Work Project',
|
|
64
|
+
color: 'blue',
|
|
65
|
+
isFavorite: true,
|
|
66
|
+
isShared: true,
|
|
67
|
+
viewStyle: 'board',
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
70
|
+
nextCursor: null,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it('should handle pagination with limit and cursor', async () => {
|
|
74
|
+
const mockProject = createMockProject({
|
|
75
|
+
id: 'project-1',
|
|
76
|
+
name: 'First Project',
|
|
77
|
+
color: 'red',
|
|
78
|
+
});
|
|
79
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([mockProject], 'next-page-cursor'));
|
|
80
|
+
const result = await projectsList.execute({ limit: 10, cursor: 'current-page-cursor' }, mockTodoistApi);
|
|
81
|
+
expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({
|
|
82
|
+
limit: 10,
|
|
83
|
+
cursor: 'current-page-cursor',
|
|
84
|
+
});
|
|
85
|
+
expect(result.projects).toHaveLength(1);
|
|
86
|
+
expect(result.projects[0]?.name).toBe('First Project');
|
|
87
|
+
expect(result.nextCursor).toBe('next-page-cursor');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('searching projects', () => {
|
|
91
|
+
it('should filter projects by search term (case insensitive)', async () => {
|
|
92
|
+
const mockProjects = [
|
|
93
|
+
createMockProject({
|
|
94
|
+
id: TEST_IDS.PROJECT_WORK,
|
|
95
|
+
name: 'Work Project',
|
|
96
|
+
color: 'blue',
|
|
97
|
+
}),
|
|
98
|
+
createMockProject({
|
|
99
|
+
id: 'personal-project-id',
|
|
100
|
+
name: 'Personal Tasks',
|
|
101
|
+
color: 'green',
|
|
102
|
+
}),
|
|
103
|
+
createMockProject({ id: 'hobby-project-id', name: 'Hobby Work', color: 'orange' }),
|
|
104
|
+
];
|
|
105
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
|
|
106
|
+
const result = await projectsList.execute({ search: 'work', limit: 50 }, mockTodoistApi);
|
|
107
|
+
expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({ limit: 50, cursor: null });
|
|
108
|
+
expect(result.projects).toHaveLength(2);
|
|
109
|
+
expect(result.projects.map((p) => p.name)).toEqual(['Work Project', 'Hobby Work']);
|
|
110
|
+
});
|
|
111
|
+
it.each([
|
|
112
|
+
{
|
|
113
|
+
search: 'nonexistent',
|
|
114
|
+
projects: ['Project One'],
|
|
115
|
+
expectedCount: 0,
|
|
116
|
+
description: 'no matches',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
search: 'IMPORTANT',
|
|
120
|
+
projects: ['Important Project'],
|
|
121
|
+
expectedCount: 1,
|
|
122
|
+
description: 'case insensitive matching',
|
|
123
|
+
},
|
|
124
|
+
])('should handle search with $description', async ({ search, projects, expectedCount }) => {
|
|
125
|
+
const mockProjects = projects.map((name) => createMockProject({ name }));
|
|
126
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
|
|
127
|
+
const result = await projectsList.execute({ search, limit: 50 }, mockTodoistApi);
|
|
128
|
+
expect(result.projects).toHaveLength(expectedCount);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('error handling', () => {
|
|
132
|
+
it.each([
|
|
133
|
+
{ error: TEST_ERRORS.API_UNAUTHORIZED, params: { limit: 50 } },
|
|
134
|
+
{ error: TEST_ERRORS.INVALID_CURSOR, params: { cursor: 'invalid-cursor', limit: 50 } },
|
|
135
|
+
])('should propagate $error', async ({ error, params }) => {
|
|
136
|
+
mockTodoistApi.getProjects.mockRejectedValue(new Error(error));
|
|
137
|
+
await expect(projectsList.execute(params, mockTodoistApi)).rejects.toThrow(error);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects-manage.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/projects-manage.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { projectsManage } from '../projects-manage.js';
|
|
3
|
+
import { TEST_IDS, createMockProject } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
addProject: jest.fn(),
|
|
7
|
+
updateProject: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
describe('projects-manage tool', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
describe('creating a new project', () => {
|
|
14
|
+
it('should create a project and return mapped result', async () => {
|
|
15
|
+
const mockApiResponse = createMockProject({
|
|
16
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
17
|
+
name: 'test-abc123def456-project',
|
|
18
|
+
childOrder: 1,
|
|
19
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
20
|
+
});
|
|
21
|
+
mockTodoistApi.addProject.mockResolvedValue(mockApiResponse);
|
|
22
|
+
const result = await projectsManage.execute({ name: 'test-abc123def456-project' }, mockTodoistApi);
|
|
23
|
+
// Verify API was called correctly
|
|
24
|
+
expect(mockTodoistApi.addProject).toHaveBeenCalledWith({
|
|
25
|
+
name: 'test-abc123def456-project',
|
|
26
|
+
});
|
|
27
|
+
// Verify result is properly mapped
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
30
|
+
name: 'test-abc123def456-project',
|
|
31
|
+
color: 'charcoal',
|
|
32
|
+
isFavorite: false,
|
|
33
|
+
isShared: false,
|
|
34
|
+
parentId: null,
|
|
35
|
+
inboxProject: false,
|
|
36
|
+
viewStyle: 'list',
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('should handle different project properties from API', async () => {
|
|
40
|
+
const mockApiResponse = createMockProject({
|
|
41
|
+
id: 'project-456',
|
|
42
|
+
name: 'My Blue Project',
|
|
43
|
+
color: 'blue',
|
|
44
|
+
isFavorite: true,
|
|
45
|
+
isShared: true,
|
|
46
|
+
parentId: 'parent-123',
|
|
47
|
+
viewStyle: 'board',
|
|
48
|
+
childOrder: 2,
|
|
49
|
+
description: 'A test project',
|
|
50
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
51
|
+
});
|
|
52
|
+
mockTodoistApi.addProject.mockResolvedValue(mockApiResponse);
|
|
53
|
+
const result = await projectsManage.execute({ name: 'My Blue Project' }, mockTodoistApi);
|
|
54
|
+
expect(mockTodoistApi.addProject).toHaveBeenCalledWith({ name: 'My Blue Project' });
|
|
55
|
+
expect(result).toEqual({
|
|
56
|
+
id: 'project-456',
|
|
57
|
+
name: 'My Blue Project',
|
|
58
|
+
color: 'blue',
|
|
59
|
+
isFavorite: true,
|
|
60
|
+
isShared: true,
|
|
61
|
+
parentId: 'parent-123',
|
|
62
|
+
inboxProject: false,
|
|
63
|
+
viewStyle: 'board',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('updating an existing project', () => {
|
|
68
|
+
it('should update a project when id is provided', async () => {
|
|
69
|
+
const mockApiResponse = {
|
|
70
|
+
url: 'https://todoist.com/projects/existing-project-123',
|
|
71
|
+
id: 'existing-project-123',
|
|
72
|
+
parentId: null,
|
|
73
|
+
isDeleted: false,
|
|
74
|
+
updatedAt: '2025-08-13T22:10:30.000000Z',
|
|
75
|
+
childOrder: 1,
|
|
76
|
+
description: '',
|
|
77
|
+
isCollapsed: false,
|
|
78
|
+
canAssignTasks: false,
|
|
79
|
+
color: 'red',
|
|
80
|
+
isFavorite: false,
|
|
81
|
+
isFrozen: false,
|
|
82
|
+
name: 'Updated Project Name',
|
|
83
|
+
viewStyle: 'list',
|
|
84
|
+
isArchived: false,
|
|
85
|
+
inboxProject: false,
|
|
86
|
+
isShared: false,
|
|
87
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
88
|
+
defaultOrder: 0,
|
|
89
|
+
};
|
|
90
|
+
mockTodoistApi.updateProject.mockResolvedValue(mockApiResponse);
|
|
91
|
+
const result = await projectsManage.execute({ id: 'existing-project-123', name: 'Updated Project Name' }, mockTodoistApi);
|
|
92
|
+
expect(mockTodoistApi.updateProject).toHaveBeenCalledWith('existing-project-123', {
|
|
93
|
+
name: 'Updated Project Name',
|
|
94
|
+
});
|
|
95
|
+
// Update returns raw project (not mapped) - this is the actual behavior
|
|
96
|
+
expect(result).toEqual(mockApiResponse);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('error handling', () => {
|
|
100
|
+
it('should propagate API errors', async () => {
|
|
101
|
+
const apiError = new Error('API Error: Project name is required');
|
|
102
|
+
mockTodoistApi.addProject.mockRejectedValue(apiError);
|
|
103
|
+
await expect(projectsManage.execute({ name: '' }, mockTodoistApi)).rejects.toThrow('API Error: Project name is required');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sections-manage.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/sections-manage.test.ts"],"names":[],"mappings":""}
|