@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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { sectionsManage } from '../sections-manage.js';
|
|
3
|
+
import { TEST_IDS, createMockSection } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
addSection: jest.fn(),
|
|
7
|
+
updateSection: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
describe('sections-manage tool', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
describe('creating a new section', () => {
|
|
14
|
+
it('should create a section and return result', async () => {
|
|
15
|
+
const mockApiResponse = createMockSection({
|
|
16
|
+
id: TEST_IDS.SECTION_1,
|
|
17
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
18
|
+
name: 'test-abc123def456-section',
|
|
19
|
+
});
|
|
20
|
+
mockTodoistApi.addSection.mockResolvedValue(mockApiResponse);
|
|
21
|
+
const result = await sectionsManage.execute({
|
|
22
|
+
name: 'test-abc123def456-section',
|
|
23
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
24
|
+
}, mockTodoistApi);
|
|
25
|
+
// Verify API was called correctly
|
|
26
|
+
expect(mockTodoistApi.addSection).toHaveBeenCalledWith({
|
|
27
|
+
name: 'test-abc123def456-section',
|
|
28
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
29
|
+
});
|
|
30
|
+
// Verify result matches API response
|
|
31
|
+
expect(result).toEqual(mockApiResponse);
|
|
32
|
+
});
|
|
33
|
+
it('should handle different section properties from API', async () => {
|
|
34
|
+
const mockApiResponse = createMockSection({
|
|
35
|
+
id: TEST_IDS.SECTION_2,
|
|
36
|
+
projectId: 'project-789',
|
|
37
|
+
sectionOrder: 2,
|
|
38
|
+
name: 'My Section Name',
|
|
39
|
+
});
|
|
40
|
+
mockTodoistApi.addSection.mockResolvedValue(mockApiResponse);
|
|
41
|
+
const result = await sectionsManage.execute({
|
|
42
|
+
name: 'My Section Name',
|
|
43
|
+
projectId: 'project-789',
|
|
44
|
+
}, mockTodoistApi);
|
|
45
|
+
expect(mockTodoistApi.addSection).toHaveBeenCalledWith({
|
|
46
|
+
name: 'My Section Name',
|
|
47
|
+
projectId: 'project-789',
|
|
48
|
+
});
|
|
49
|
+
expect(result).toEqual(mockApiResponse);
|
|
50
|
+
});
|
|
51
|
+
it('should return error when projectId is missing for new section', async () => {
|
|
52
|
+
const result = await sectionsManage.execute({
|
|
53
|
+
name: 'test-section',
|
|
54
|
+
}, mockTodoistApi);
|
|
55
|
+
// Should not call API when projectId is missing
|
|
56
|
+
expect(mockTodoistApi.addSection).not.toHaveBeenCalled();
|
|
57
|
+
// Should return error content
|
|
58
|
+
expect(result).toEqual({
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: 'Error: projectId is required when creating a new section (when id is not provided).',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
isError: true,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('updating an existing section', () => {
|
|
70
|
+
it('should update a section when id is provided', async () => {
|
|
71
|
+
const mockApiResponse = {
|
|
72
|
+
id: 'existing-section-123',
|
|
73
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
74
|
+
sectionOrder: 1,
|
|
75
|
+
userId: 'test-user',
|
|
76
|
+
addedAt: '2024-01-01T00:00:00Z',
|
|
77
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
78
|
+
archivedAt: null,
|
|
79
|
+
isArchived: false,
|
|
80
|
+
isDeleted: false,
|
|
81
|
+
isCollapsed: false,
|
|
82
|
+
name: 'Updated Section Name',
|
|
83
|
+
};
|
|
84
|
+
mockTodoistApi.updateSection.mockResolvedValue(mockApiResponse);
|
|
85
|
+
const result = await sectionsManage.execute({
|
|
86
|
+
id: 'existing-section-123',
|
|
87
|
+
name: 'Updated Section Name',
|
|
88
|
+
}, mockTodoistApi);
|
|
89
|
+
expect(mockTodoistApi.updateSection).toHaveBeenCalledWith('existing-section-123', {
|
|
90
|
+
name: 'Updated Section Name',
|
|
91
|
+
});
|
|
92
|
+
expect(result).toEqual(mockApiResponse);
|
|
93
|
+
});
|
|
94
|
+
it('should update section without requiring projectId', async () => {
|
|
95
|
+
const mockApiResponse = {
|
|
96
|
+
id: 'section-update-test',
|
|
97
|
+
projectId: 'original-project-id',
|
|
98
|
+
sectionOrder: 3,
|
|
99
|
+
userId: 'test-user',
|
|
100
|
+
addedAt: '2024-01-01T00:00:00Z',
|
|
101
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
102
|
+
archivedAt: null,
|
|
103
|
+
isArchived: false,
|
|
104
|
+
isDeleted: false,
|
|
105
|
+
isCollapsed: false,
|
|
106
|
+
name: 'Section New Name',
|
|
107
|
+
};
|
|
108
|
+
mockTodoistApi.updateSection.mockResolvedValue(mockApiResponse);
|
|
109
|
+
const result = await sectionsManage.execute({
|
|
110
|
+
id: 'section-update-test',
|
|
111
|
+
name: 'Section New Name',
|
|
112
|
+
// Note: projectId not provided for update
|
|
113
|
+
}, mockTodoistApi);
|
|
114
|
+
expect(mockTodoistApi.updateSection).toHaveBeenCalledWith('section-update-test', {
|
|
115
|
+
name: 'Section New Name',
|
|
116
|
+
});
|
|
117
|
+
expect(result).toEqual(mockApiResponse);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('error handling', () => {
|
|
121
|
+
it('should propagate API errors for section creation', async () => {
|
|
122
|
+
const apiError = new Error('API Error: Section name is required');
|
|
123
|
+
mockTodoistApi.addSection.mockRejectedValue(apiError);
|
|
124
|
+
await expect(sectionsManage.execute({
|
|
125
|
+
name: '',
|
|
126
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
127
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Section name is required');
|
|
128
|
+
});
|
|
129
|
+
it('should propagate API errors for section updates', async () => {
|
|
130
|
+
const apiError = new Error('API Error: Section not found');
|
|
131
|
+
mockTodoistApi.updateSection.mockRejectedValue(apiError);
|
|
132
|
+
await expect(sectionsManage.execute({
|
|
133
|
+
id: 'non-existent-section',
|
|
134
|
+
name: 'Updated Name',
|
|
135
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Section not found');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sections-search.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/sections-search.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { sectionsSearch } from '../sections-search.js';
|
|
3
|
+
import { TEST_ERRORS, TEST_IDS, createMockSection } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
getSections: jest.fn(),
|
|
7
|
+
};
|
|
8
|
+
describe('sections-search tool', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
describe('listing all sections in a project', () => {
|
|
13
|
+
it('should list all sections when no search parameter is provided', async () => {
|
|
14
|
+
const mockSections = [
|
|
15
|
+
createMockSection({
|
|
16
|
+
id: TEST_IDS.SECTION_1,
|
|
17
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
18
|
+
name: 'To Do',
|
|
19
|
+
}),
|
|
20
|
+
createMockSection({
|
|
21
|
+
id: TEST_IDS.SECTION_2,
|
|
22
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
23
|
+
sectionOrder: 2,
|
|
24
|
+
name: 'In Progress',
|
|
25
|
+
}),
|
|
26
|
+
createMockSection({
|
|
27
|
+
id: 'section-789',
|
|
28
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
29
|
+
sectionOrder: 3,
|
|
30
|
+
name: 'Done',
|
|
31
|
+
}),
|
|
32
|
+
createMockSection({
|
|
33
|
+
id: 'section-999',
|
|
34
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
35
|
+
sectionOrder: 4,
|
|
36
|
+
name: 'Backlog Items',
|
|
37
|
+
}),
|
|
38
|
+
];
|
|
39
|
+
mockTodoistApi.getSections.mockResolvedValue({
|
|
40
|
+
results: mockSections,
|
|
41
|
+
nextCursor: null,
|
|
42
|
+
});
|
|
43
|
+
const result = await sectionsSearch.execute({ projectId: TEST_IDS.PROJECT_TEST }, mockTodoistApi);
|
|
44
|
+
// Verify API was called correctly
|
|
45
|
+
expect(mockTodoistApi.getSections).toHaveBeenCalledWith({
|
|
46
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
47
|
+
});
|
|
48
|
+
// Verify result is properly mapped (simplified format)
|
|
49
|
+
expect(result).toEqual([
|
|
50
|
+
{ id: TEST_IDS.SECTION_1, name: 'To Do' },
|
|
51
|
+
{ id: TEST_IDS.SECTION_2, name: 'In Progress' },
|
|
52
|
+
{ id: 'section-789', name: 'Done' },
|
|
53
|
+
{ id: 'section-999', name: 'Backlog Items' },
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
it('should handle project with no sections', async () => {
|
|
57
|
+
mockTodoistApi.getSections.mockResolvedValue({
|
|
58
|
+
results: [],
|
|
59
|
+
nextCursor: null,
|
|
60
|
+
});
|
|
61
|
+
const result = await sectionsSearch.execute({ projectId: 'empty-project-id' }, mockTodoistApi);
|
|
62
|
+
expect(mockTodoistApi.getSections).toHaveBeenCalledWith({
|
|
63
|
+
projectId: 'empty-project-id',
|
|
64
|
+
});
|
|
65
|
+
expect(result).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('searching sections by name', () => {
|
|
69
|
+
it('should filter sections by search term (case insensitive)', async () => {
|
|
70
|
+
const mockSections = [
|
|
71
|
+
createMockSection({
|
|
72
|
+
id: TEST_IDS.SECTION_1,
|
|
73
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
74
|
+
name: 'To Do',
|
|
75
|
+
}),
|
|
76
|
+
createMockSection({
|
|
77
|
+
id: TEST_IDS.SECTION_2,
|
|
78
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
79
|
+
sectionOrder: 2,
|
|
80
|
+
name: 'In Progress',
|
|
81
|
+
}),
|
|
82
|
+
createMockSection({
|
|
83
|
+
id: 'section-789',
|
|
84
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
85
|
+
sectionOrder: 3,
|
|
86
|
+
name: 'Done',
|
|
87
|
+
}),
|
|
88
|
+
createMockSection({
|
|
89
|
+
id: 'section-999',
|
|
90
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
91
|
+
sectionOrder: 4,
|
|
92
|
+
name: 'Progress Review',
|
|
93
|
+
}),
|
|
94
|
+
];
|
|
95
|
+
mockTodoistApi.getSections.mockResolvedValue({
|
|
96
|
+
results: mockSections,
|
|
97
|
+
nextCursor: null,
|
|
98
|
+
});
|
|
99
|
+
const result = await sectionsSearch.execute({
|
|
100
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
101
|
+
search: 'progress',
|
|
102
|
+
}, mockTodoistApi);
|
|
103
|
+
expect(mockTodoistApi.getSections).toHaveBeenCalledWith({
|
|
104
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
105
|
+
});
|
|
106
|
+
// Should return both "In Progress" and "Progress Review" (case insensitive partial match)
|
|
107
|
+
expect(result).toEqual([
|
|
108
|
+
{ id: TEST_IDS.SECTION_2, name: 'In Progress' },
|
|
109
|
+
{ id: 'section-999', name: 'Progress Review' },
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
it('should handle search with no matches', async () => {
|
|
113
|
+
const mockSections = [
|
|
114
|
+
createMockSection({
|
|
115
|
+
id: TEST_IDS.SECTION_1,
|
|
116
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
117
|
+
name: 'To Do',
|
|
118
|
+
}),
|
|
119
|
+
createMockSection({
|
|
120
|
+
id: TEST_IDS.SECTION_2,
|
|
121
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
122
|
+
sectionOrder: 2,
|
|
123
|
+
name: 'In Progress',
|
|
124
|
+
}),
|
|
125
|
+
];
|
|
126
|
+
mockTodoistApi.getSections.mockResolvedValue({
|
|
127
|
+
results: mockSections,
|
|
128
|
+
nextCursor: null,
|
|
129
|
+
});
|
|
130
|
+
const result = await sectionsSearch.execute({
|
|
131
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
132
|
+
search: 'nonexistent',
|
|
133
|
+
}, mockTodoistApi);
|
|
134
|
+
expect(result).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
it('should handle case sensitive search correctly', async () => {
|
|
137
|
+
const mockSections = [
|
|
138
|
+
createMockSection({
|
|
139
|
+
id: TEST_IDS.SECTION_1,
|
|
140
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
141
|
+
name: 'Important Tasks',
|
|
142
|
+
}),
|
|
143
|
+
createMockSection({
|
|
144
|
+
id: TEST_IDS.SECTION_2,
|
|
145
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
146
|
+
sectionOrder: 2,
|
|
147
|
+
name: 'Regular Work',
|
|
148
|
+
}),
|
|
149
|
+
];
|
|
150
|
+
mockTodoistApi.getSections.mockResolvedValue({
|
|
151
|
+
results: mockSections,
|
|
152
|
+
nextCursor: null,
|
|
153
|
+
});
|
|
154
|
+
const result = await sectionsSearch.execute({
|
|
155
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
156
|
+
search: 'IMPORTANT',
|
|
157
|
+
}, mockTodoistApi);
|
|
158
|
+
// Should match despite different case
|
|
159
|
+
expect(result).toHaveLength(1);
|
|
160
|
+
expect(result[0]).toEqual({ id: TEST_IDS.SECTION_1, name: 'Important Tasks' });
|
|
161
|
+
});
|
|
162
|
+
it('should handle partial matches correctly', async () => {
|
|
163
|
+
const mockSections = [
|
|
164
|
+
createMockSection({
|
|
165
|
+
id: TEST_IDS.SECTION_1,
|
|
166
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
167
|
+
name: 'Development Tasks',
|
|
168
|
+
}),
|
|
169
|
+
createMockSection({
|
|
170
|
+
id: TEST_IDS.SECTION_2,
|
|
171
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
172
|
+
sectionOrder: 2,
|
|
173
|
+
name: 'Testing Tasks',
|
|
174
|
+
}),
|
|
175
|
+
createMockSection({
|
|
176
|
+
id: 'section-789',
|
|
177
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
178
|
+
sectionOrder: 3,
|
|
179
|
+
name: 'Deployment',
|
|
180
|
+
}),
|
|
181
|
+
];
|
|
182
|
+
mockTodoistApi.getSections.mockResolvedValue({
|
|
183
|
+
results: mockSections,
|
|
184
|
+
nextCursor: null,
|
|
185
|
+
});
|
|
186
|
+
const result = await sectionsSearch.execute({
|
|
187
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
188
|
+
search: 'task',
|
|
189
|
+
}, mockTodoistApi);
|
|
190
|
+
// Should match both sections with "task" in the name
|
|
191
|
+
expect(result).toEqual([
|
|
192
|
+
{ id: TEST_IDS.SECTION_1, name: 'Development Tasks' },
|
|
193
|
+
{ id: TEST_IDS.SECTION_2, name: 'Testing Tasks' },
|
|
194
|
+
]);
|
|
195
|
+
});
|
|
196
|
+
it('should handle exact matches', async () => {
|
|
197
|
+
const mockSections = [
|
|
198
|
+
createMockSection({
|
|
199
|
+
id: TEST_IDS.SECTION_1,
|
|
200
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
201
|
+
name: 'Done',
|
|
202
|
+
}),
|
|
203
|
+
createMockSection({
|
|
204
|
+
id: TEST_IDS.SECTION_2,
|
|
205
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
206
|
+
sectionOrder: 2,
|
|
207
|
+
name: 'Done Soon',
|
|
208
|
+
}),
|
|
209
|
+
];
|
|
210
|
+
mockTodoistApi.getSections.mockResolvedValue({
|
|
211
|
+
results: mockSections,
|
|
212
|
+
nextCursor: null,
|
|
213
|
+
});
|
|
214
|
+
const result = await sectionsSearch.execute({
|
|
215
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
216
|
+
search: 'done',
|
|
217
|
+
}, mockTodoistApi);
|
|
218
|
+
// Should match both sections containing "done"
|
|
219
|
+
expect(result).toEqual([
|
|
220
|
+
{ id: TEST_IDS.SECTION_1, name: 'Done' },
|
|
221
|
+
{ id: TEST_IDS.SECTION_2, name: 'Done Soon' },
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('error handling', () => {
|
|
226
|
+
it.each([
|
|
227
|
+
{ error: 'API Error: Project not found', projectId: 'non-existent-project' },
|
|
228
|
+
{ error: TEST_ERRORS.API_UNAUTHORIZED, projectId: 'restricted-project' },
|
|
229
|
+
{ error: 'API Error: Invalid project ID format', projectId: 'invalid-id-format' },
|
|
230
|
+
])('should propagate $error', async ({ error, projectId }) => {
|
|
231
|
+
mockTodoistApi.getSections.mockRejectedValue(new Error(error));
|
|
232
|
+
await expect(sectionsSearch.execute({ projectId }, mockTodoistApi)).rejects.toThrow(error);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-add-multiple.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-add-multiple.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { tasksAddMultiple } from '../tasks-add-multiple.js';
|
|
3
|
+
import { createMockTask } from '../test-helpers.js';
|
|
4
|
+
// Mock the Todoist API
|
|
5
|
+
const mockTodoistApi = {
|
|
6
|
+
addTask: jest.fn(),
|
|
7
|
+
};
|
|
8
|
+
describe('tasks-add-multiple tool', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
describe('adding multiple tasks', () => {
|
|
13
|
+
it('should add multiple tasks and return mapped results', async () => {
|
|
14
|
+
// Mock API responses extracted from recordings (Task type)
|
|
15
|
+
const mockApiResponse1 = createMockTask({
|
|
16
|
+
id: '8485093748',
|
|
17
|
+
content: 'First task content',
|
|
18
|
+
url: 'https://todoist.com/showTask?id=8485093748',
|
|
19
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
20
|
+
});
|
|
21
|
+
const mockApiResponse2 = createMockTask({
|
|
22
|
+
id: '8485093749',
|
|
23
|
+
content: 'Second task content',
|
|
24
|
+
description: 'Task description',
|
|
25
|
+
labels: ['work', 'urgent'],
|
|
26
|
+
childOrder: 2,
|
|
27
|
+
priority: 2,
|
|
28
|
+
url: 'https://todoist.com/showTask?id=8485093749',
|
|
29
|
+
addedAt: '2025-08-13T22:09:57.123456Z',
|
|
30
|
+
due: {
|
|
31
|
+
date: '2025-08-15',
|
|
32
|
+
isRecurring: false,
|
|
33
|
+
lang: 'en',
|
|
34
|
+
string: 'Aug 15',
|
|
35
|
+
timezone: null,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
mockTodoistApi.addTask
|
|
39
|
+
.mockResolvedValueOnce(mockApiResponse1)
|
|
40
|
+
.mockResolvedValueOnce(mockApiResponse2);
|
|
41
|
+
const result = await tasksAddMultiple.execute({
|
|
42
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
43
|
+
tasks: [
|
|
44
|
+
{ content: 'First task content' },
|
|
45
|
+
{
|
|
46
|
+
content: 'Second task content',
|
|
47
|
+
description: 'Task description',
|
|
48
|
+
priority: 2,
|
|
49
|
+
dueString: 'Aug 15',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}, mockTodoistApi);
|
|
53
|
+
// Verify API was called correctly for each task
|
|
54
|
+
expect(mockTodoistApi.addTask).toHaveBeenCalledTimes(2);
|
|
55
|
+
expect(mockTodoistApi.addTask).toHaveBeenNthCalledWith(1, {
|
|
56
|
+
content: 'First task content',
|
|
57
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
58
|
+
sectionId: undefined,
|
|
59
|
+
parentId: undefined,
|
|
60
|
+
});
|
|
61
|
+
expect(mockTodoistApi.addTask).toHaveBeenNthCalledWith(2, {
|
|
62
|
+
content: 'Second task content',
|
|
63
|
+
description: 'Task description',
|
|
64
|
+
priority: 2,
|
|
65
|
+
dueString: 'Aug 15',
|
|
66
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
67
|
+
sectionId: undefined,
|
|
68
|
+
parentId: undefined,
|
|
69
|
+
});
|
|
70
|
+
// Verify result is properly mapped
|
|
71
|
+
expect(result).toEqual([
|
|
72
|
+
expect.objectContaining({
|
|
73
|
+
id: '8485093748',
|
|
74
|
+
content: 'First task content',
|
|
75
|
+
description: '',
|
|
76
|
+
labels: [],
|
|
77
|
+
}),
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
id: '8485093749',
|
|
80
|
+
content: 'Second task content',
|
|
81
|
+
description: 'Task description',
|
|
82
|
+
dueDate: '2025-08-15',
|
|
83
|
+
priority: 2,
|
|
84
|
+
labels: ['work', 'urgent'],
|
|
85
|
+
}),
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
it('should handle tasks with section and parent IDs', async () => {
|
|
89
|
+
const mockApiResponse = createMockTask({
|
|
90
|
+
id: '8485093750',
|
|
91
|
+
content: 'Subtask content',
|
|
92
|
+
description: 'Subtask description',
|
|
93
|
+
priority: 3,
|
|
94
|
+
sectionId: 'section-123',
|
|
95
|
+
parentId: 'parent-task-456',
|
|
96
|
+
url: 'https://todoist.com/showTask?id=8485093750',
|
|
97
|
+
addedAt: '2025-08-13T22:09:58.123456Z',
|
|
98
|
+
});
|
|
99
|
+
mockTodoistApi.addTask.mockResolvedValue(mockApiResponse);
|
|
100
|
+
const result = await tasksAddMultiple.execute({
|
|
101
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
102
|
+
sectionId: 'section-123',
|
|
103
|
+
parentId: 'parent-task-456',
|
|
104
|
+
tasks: [
|
|
105
|
+
{
|
|
106
|
+
content: 'Subtask content',
|
|
107
|
+
description: 'Subtask description',
|
|
108
|
+
priority: 3,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}, mockTodoistApi);
|
|
112
|
+
expect(mockTodoistApi.addTask).toHaveBeenCalledWith({
|
|
113
|
+
content: 'Subtask content',
|
|
114
|
+
description: 'Subtask description',
|
|
115
|
+
priority: 3,
|
|
116
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
117
|
+
sectionId: 'section-123',
|
|
118
|
+
parentId: 'parent-task-456',
|
|
119
|
+
});
|
|
120
|
+
expect(result).toEqual([
|
|
121
|
+
expect.objectContaining({
|
|
122
|
+
id: '8485093750',
|
|
123
|
+
content: 'Subtask content',
|
|
124
|
+
description: 'Subtask description',
|
|
125
|
+
priority: 3,
|
|
126
|
+
sectionId: 'section-123',
|
|
127
|
+
parentId: 'parent-task-456',
|
|
128
|
+
labels: [],
|
|
129
|
+
}),
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
it('should add tasks with duration', async () => {
|
|
133
|
+
const mockApiResponse1 = createMockTask({
|
|
134
|
+
id: '8485093752',
|
|
135
|
+
content: 'Task with 2 hour duration',
|
|
136
|
+
duration: { amount: 120, unit: 'minute' },
|
|
137
|
+
url: 'https://todoist.com/showTask?id=8485093752',
|
|
138
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
139
|
+
});
|
|
140
|
+
const mockApiResponse2 = createMockTask({
|
|
141
|
+
id: '8485093753',
|
|
142
|
+
content: 'Task with 45 minute duration',
|
|
143
|
+
duration: { amount: 45, unit: 'minute' },
|
|
144
|
+
url: 'https://todoist.com/showTask?id=8485093753',
|
|
145
|
+
addedAt: '2025-08-13T22:09:57.123456Z',
|
|
146
|
+
});
|
|
147
|
+
mockTodoistApi.addTask
|
|
148
|
+
.mockResolvedValueOnce(mockApiResponse1)
|
|
149
|
+
.mockResolvedValueOnce(mockApiResponse2);
|
|
150
|
+
const result = await tasksAddMultiple.execute({
|
|
151
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
152
|
+
tasks: [
|
|
153
|
+
{
|
|
154
|
+
content: 'Task with 2 hour duration',
|
|
155
|
+
duration: '2h',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
content: 'Task with 45 minute duration',
|
|
159
|
+
duration: '45m',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
}, mockTodoistApi);
|
|
163
|
+
// Verify API was called with parsed duration
|
|
164
|
+
expect(mockTodoistApi.addTask).toHaveBeenNthCalledWith(1, {
|
|
165
|
+
content: 'Task with 2 hour duration',
|
|
166
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
167
|
+
sectionId: undefined,
|
|
168
|
+
parentId: undefined,
|
|
169
|
+
duration: 120,
|
|
170
|
+
durationUnit: 'minute',
|
|
171
|
+
});
|
|
172
|
+
expect(mockTodoistApi.addTask).toHaveBeenNthCalledWith(2, {
|
|
173
|
+
content: 'Task with 45 minute duration',
|
|
174
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
175
|
+
sectionId: undefined,
|
|
176
|
+
parentId: undefined,
|
|
177
|
+
duration: 45,
|
|
178
|
+
durationUnit: 'minute',
|
|
179
|
+
});
|
|
180
|
+
// Verify result includes formatted duration
|
|
181
|
+
expect(result).toEqual([
|
|
182
|
+
expect.objectContaining({
|
|
183
|
+
id: '8485093752',
|
|
184
|
+
content: 'Task with 2 hour duration',
|
|
185
|
+
duration: '2h',
|
|
186
|
+
}),
|
|
187
|
+
expect.objectContaining({
|
|
188
|
+
id: '8485093753',
|
|
189
|
+
content: 'Task with 45 minute duration',
|
|
190
|
+
duration: '45m',
|
|
191
|
+
}),
|
|
192
|
+
]);
|
|
193
|
+
});
|
|
194
|
+
it('should handle various duration formats', async () => {
|
|
195
|
+
const mockApiResponse = createMockTask({
|
|
196
|
+
id: '8485093754',
|
|
197
|
+
content: 'Task with combined duration',
|
|
198
|
+
duration: { amount: 150, unit: 'minute' },
|
|
199
|
+
url: 'https://todoist.com/showTask?id=8485093754',
|
|
200
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
201
|
+
});
|
|
202
|
+
mockTodoistApi.addTask.mockResolvedValue(mockApiResponse);
|
|
203
|
+
// Test different duration formats
|
|
204
|
+
const testCases = [
|
|
205
|
+
{ input: '2h30m', expectedMinutes: 150 },
|
|
206
|
+
{ input: '1.5h', expectedMinutes: 90 },
|
|
207
|
+
{ input: ' 90m ', expectedMinutes: 90 },
|
|
208
|
+
{ input: '2H30M', expectedMinutes: 150 },
|
|
209
|
+
];
|
|
210
|
+
for (const testCase of testCases) {
|
|
211
|
+
mockTodoistApi.addTask.mockClear();
|
|
212
|
+
await tasksAddMultiple.execute({
|
|
213
|
+
tasks: [
|
|
214
|
+
{
|
|
215
|
+
content: 'Test task',
|
|
216
|
+
duration: testCase.input,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
}, mockTodoistApi);
|
|
220
|
+
expect(mockTodoistApi.addTask).toHaveBeenCalledWith(expect.objectContaining({
|
|
221
|
+
duration: testCase.expectedMinutes,
|
|
222
|
+
durationUnit: 'minute',
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe('error handling', () => {
|
|
228
|
+
it('should throw error for invalid duration format', async () => {
|
|
229
|
+
await expect(tasksAddMultiple.execute({
|
|
230
|
+
tasks: [
|
|
231
|
+
{
|
|
232
|
+
content: 'Task with invalid duration',
|
|
233
|
+
duration: 'invalid',
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
}, mockTodoistApi)).rejects.toThrow('Task "Task with invalid duration": Invalid duration format "invalid"');
|
|
237
|
+
});
|
|
238
|
+
it('should throw error for duration exceeding 24 hours', async () => {
|
|
239
|
+
await expect(tasksAddMultiple.execute({
|
|
240
|
+
tasks: [
|
|
241
|
+
{
|
|
242
|
+
content: 'Task with too long duration',
|
|
243
|
+
duration: '25h',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
}, mockTodoistApi)).rejects.toThrow('Task "Task with too long duration": Invalid duration format "25h": Duration cannot exceed 24 hours (1440 minutes)');
|
|
247
|
+
});
|
|
248
|
+
it('should propagate API errors', async () => {
|
|
249
|
+
const apiError = new Error('API Error: Task content is required');
|
|
250
|
+
mockTodoistApi.addTask.mockRejectedValue(apiError);
|
|
251
|
+
await expect(tasksAddMultiple.execute({ tasks: [{ content: '' }] }, mockTodoistApi)).rejects.toThrow(apiError.message);
|
|
252
|
+
});
|
|
253
|
+
it('should handle partial failures when adding multiple tasks', async () => {
|
|
254
|
+
const mockApiResponse = createMockTask({
|
|
255
|
+
id: '8485093751',
|
|
256
|
+
content: 'First task content',
|
|
257
|
+
url: 'https://todoist.com/showTask?id=8485093751',
|
|
258
|
+
addedAt: '2025-08-13T22:09:59.123456Z',
|
|
259
|
+
});
|
|
260
|
+
const apiError = new Error('API Error: Second task failed');
|
|
261
|
+
mockTodoistApi.addTask
|
|
262
|
+
.mockResolvedValueOnce(mockApiResponse)
|
|
263
|
+
.mockRejectedValueOnce(apiError);
|
|
264
|
+
await expect(tasksAddMultiple.execute({
|
|
265
|
+
tasks: [
|
|
266
|
+
{ content: 'First task content' },
|
|
267
|
+
{ content: 'Second task content' },
|
|
268
|
+
],
|
|
269
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Second task failed');
|
|
270
|
+
// Verify first task was attempted
|
|
271
|
+
expect(mockTodoistApi.addTask).toHaveBeenCalledTimes(2);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-complete-multiple.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/tasks-complete-multiple.test.ts"],"names":[],"mappings":""}
|