From eba62dfa479fa0626748fd8d74f102520bb35a2f Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Mon, 30 Mar 2026 20:07:12 +0000 Subject: [PATCH 1/3] feat(tasks): add support for Google Tasks Adds full support for Google Tasks with 6 new tools: - tasks.listLists: List task lists - tasks.list: List tasks with filtering (completed, assigned, due dates) - tasks.create: Create new tasks - tasks.update: Update existing tasks - tasks.complete: Mark tasks as completed - tasks.delete: Delete tasks Follows existing service patterns with private client method, error handling, and logging. Tasks scopes (default-OFF) will be gated by the feature configuration service. Fixes #105 --- workspace-server/WORKSPACE-Context.md | 13 + .../__tests__/services/TasksService.test.ts | 314 ++++++++++++++++++ workspace-server/src/index.ts | 133 ++++++++ workspace-server/src/services/TasksService.ts | 254 ++++++++++++++ 4 files changed, 714 insertions(+) create mode 100644 workspace-server/src/__tests__/services/TasksService.test.ts create mode 100644 workspace-server/src/services/TasksService.ts diff --git a/workspace-server/WORKSPACE-Context.md b/workspace-server/WORKSPACE-Context.md index d7eaf1b..6e6f887 100644 --- a/workspace-server/WORKSPACE-Context.md +++ b/workspace-server/WORKSPACE-Context.md @@ -200,6 +200,19 @@ filter rather than searching by name alone. Example MIME type queries: - See the **Google Chat skill** for detailed guidance on formatting messages, spaces vs. DMs, threading, unread filtering, and space management. +### Google Tasks + +- **Task List Selection**: If the user doesn't specify a task list, default to + listing all task lists first to let them choose, or ask for clarification. +- **Task Creation**: When creating tasks, prompt for a due date if one isn't + provided, as it's helpful for organization. +- **Completion**: Use `tasks.complete` for a simple "mark as done" action. Use + `tasks.update` if you need to set other properties simultaneously. +- **Assigned Tasks**: To find tasks assigned from Google Docs or Chat, use + `showAssigned=true` when listing tasks. +- **Timestamps**: Ensure due dates are in RFC 3339 format (e.g., + `2024-01-15T12:00:00Z`). + Remember: This guide focuses on **how to think** about using these tools effectively. For specific parameter details, refer to the tool descriptions themselves. diff --git a/workspace-server/src/__tests__/services/TasksService.test.ts b/workspace-server/src/__tests__/services/TasksService.test.ts new file mode 100644 index 0000000..68bf6ef --- /dev/null +++ b/workspace-server/src/__tests__/services/TasksService.test.ts @@ -0,0 +1,314 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + describe, + it, + expect, + jest, + beforeEach, + afterEach, +} from '@jest/globals'; +import { TasksService } from '../../services/TasksService'; +import { AuthManager } from '../../auth/AuthManager'; +import { google } from 'googleapis'; + +// Mock the googleapis module +jest.mock('googleapis'); +jest.mock('../../utils/logger'); + +describe('TasksService', () => { + let tasksService: TasksService; + let mockAuthManager: jest.Mocked; + let mockTasksAPI: any; + + beforeEach(() => { + jest.clearAllMocks(); + + mockAuthManager = { + getAuthenticatedClient: jest.fn(), + } as any; + + mockTasksAPI = { + tasklists: { + list: jest.fn(), + }, + tasks: { + list: jest.fn(), + insert: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + }, + }; + + (google.tasks as jest.Mock) = jest.fn().mockReturnValue(mockTasksAPI); + + tasksService = new TasksService(mockAuthManager); + + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue( + mockAuthClient as any, + ); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('listTaskLists', () => { + it('should list task lists', async () => { + const mockItems = [{ id: 'list1', title: 'My Tasks' }]; + mockTasksAPI.tasklists.list.mockResolvedValue({ + data: { items: mockItems }, + }); + + const result = await tasksService.listTaskLists(); + + expect(mockTasksAPI.tasklists.list).toHaveBeenCalledWith({ + maxResults: undefined, + pageToken: undefined, + }); + expect(JSON.parse(result.content[0].text)).toEqual(mockItems); + }); + + it('should pass pagination parameters', async () => { + mockTasksAPI.tasklists.list.mockResolvedValue({ + data: { items: [] }, + }); + + await tasksService.listTaskLists({ maxResults: 10, pageToken: 'token' }); + + expect(mockTasksAPI.tasklists.list).toHaveBeenCalledWith({ + maxResults: 10, + pageToken: 'token', + }); + }); + + it('should return empty array when no items', async () => { + mockTasksAPI.tasklists.list.mockResolvedValue({ + data: {}, + }); + + const result = await tasksService.listTaskLists(); + + expect(JSON.parse(result.content[0].text)).toEqual([]); + }); + + it('should handle API errors gracefully', async () => { + mockTasksAPI.tasklists.list.mockRejectedValue( + new Error('Tasks API failed'), + ); + + const result = await tasksService.listTaskLists(); + + expect(JSON.parse(result.content[0].text)).toEqual({ + error: 'Tasks API failed', + }); + }); + }); + + describe('listTasks', () => { + it('should list tasks in a task list', async () => { + const mockItems = [{ id: 'task1', title: 'Buy milk' }]; + mockTasksAPI.tasks.list.mockResolvedValue({ + data: { items: mockItems }, + }); + + const result = await tasksService.listTasks({ + taskListId: 'list1', + showAssigned: true, + }); + + expect(mockTasksAPI.tasks.list).toHaveBeenCalledWith({ + tasklist: 'list1', + showCompleted: undefined, + showDeleted: undefined, + showHidden: undefined, + showAssigned: true, + maxResults: undefined, + pageToken: undefined, + dueMin: undefined, + dueMax: undefined, + }); + expect(JSON.parse(result.content[0].text)).toEqual(mockItems); + }); + + it('should handle API errors gracefully', async () => { + mockTasksAPI.tasks.list.mockRejectedValue(new Error('Tasks API failed')); + + const result = await tasksService.listTasks({ taskListId: 'list1' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ + error: 'Tasks API failed', + }); + }); + }); + + describe('createTask', () => { + it('should create a task with title only', async () => { + const mockResponse = { id: 'task1', title: 'New Task' }; + mockTasksAPI.tasks.insert.mockResolvedValue({ + data: mockResponse, + }); + + const result = await tasksService.createTask({ + taskListId: 'list1', + title: 'New Task', + }); + + expect(mockTasksAPI.tasks.insert).toHaveBeenCalledWith({ + tasklist: 'list1', + requestBody: { + title: 'New Task', + }, + }); + expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); + }); + + it('should create a task with notes and due date', async () => { + const mockResponse = { + id: 'task1', + title: 'New Task', + notes: 'Some notes', + due: '2024-01-15T12:00:00Z', + }; + mockTasksAPI.tasks.insert.mockResolvedValue({ + data: mockResponse, + }); + + const result = await tasksService.createTask({ + taskListId: 'list1', + title: 'New Task', + notes: 'Some notes', + due: '2024-01-15T12:00:00Z', + }); + + expect(mockTasksAPI.tasks.insert).toHaveBeenCalledWith({ + tasklist: 'list1', + requestBody: { + title: 'New Task', + notes: 'Some notes', + due: '2024-01-15T12:00:00Z', + }, + }); + expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); + }); + + it('should handle API errors gracefully', async () => { + mockTasksAPI.tasks.insert.mockRejectedValue( + new Error('Tasks API failed'), + ); + + const result = await tasksService.createTask({ + taskListId: 'list1', + title: 'New Task', + }); + + expect(JSON.parse(result.content[0].text)).toEqual({ + error: 'Tasks API failed', + }); + }); + }); + + describe('updateTask', () => { + it('should update a task', async () => { + const mockResponse = { id: 'task1', title: 'Updated Task' }; + mockTasksAPI.tasks.patch.mockResolvedValue({ + data: mockResponse, + }); + + const result = await tasksService.updateTask({ + taskListId: 'list1', + taskId: 'task1', + title: 'Updated Task', + }); + + expect(mockTasksAPI.tasks.patch).toHaveBeenCalledWith({ + tasklist: 'list1', + task: 'task1', + requestBody: { + title: 'Updated Task', + }, + }); + expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); + }); + + it('should handle API errors gracefully', async () => { + mockTasksAPI.tasks.patch.mockRejectedValue(new Error('Tasks API failed')); + + const result = await tasksService.updateTask({ + taskListId: 'list1', + taskId: 'task1', + title: 'Updated Task', + }); + + expect(JSON.parse(result.content[0].text)).toEqual({ + error: 'Tasks API failed', + }); + }); + }); + + describe('completeTask', () => { + it('should mark a task as completed', async () => { + const mockResponse = { + id: 'task1', + title: 'Task 1', + status: 'completed', + }; + mockTasksAPI.tasks.patch.mockResolvedValue({ + data: mockResponse, + }); + + const result = await tasksService.completeTask({ + taskListId: 'list1', + taskId: 'task1', + }); + + expect(mockTasksAPI.tasks.patch).toHaveBeenCalledWith({ + tasklist: 'list1', + task: 'task1', + requestBody: { + status: 'completed', + }, + }); + expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); + }); + }); + + describe('deleteTask', () => { + it('should delete a task', async () => { + mockTasksAPI.tasks.delete.mockResolvedValue({}); + + const result = await tasksService.deleteTask({ + taskListId: 'list1', + taskId: 'task1', + }); + + expect(mockTasksAPI.tasks.delete).toHaveBeenCalledWith({ + tasklist: 'list1', + task: 'task1', + }); + expect(result.content[0].text).toBe( + 'Task task1 deleted successfully from list list1.', + ); + }); + + it('should handle API errors gracefully', async () => { + mockTasksAPI.tasks.delete.mockRejectedValue( + new Error('Tasks API failed'), + ); + + const result = await tasksService.deleteTask({ + taskListId: 'list1', + taskId: 'task1', + }); + + expect(JSON.parse(result.content[0].text)).toEqual({ + error: 'Tasks API failed', + }); + }); + }); +}); diff --git a/workspace-server/src/index.ts b/workspace-server/src/index.ts index 674d678..6b06952 100644 --- a/workspace-server/src/index.ts +++ b/workspace-server/src/index.ts @@ -19,6 +19,7 @@ import { TimeService } from './services/TimeService'; import { PeopleService } from './services/PeopleService'; import { SlidesService } from './services/SlidesService'; import { SheetsService } from './services/SheetsService'; +import { TasksService } from './services/TasksService'; import { GMAIL_SEARCH_MAX_RESULTS } from './utils/constants'; import { setLoggingEnabled } from './utils/logger'; @@ -128,6 +129,7 @@ async function main() { const timeService = new TimeService(); const slidesService = new SlidesService(authManager); const sheetsService = new SheetsService(authManager); + const tasksService = new TasksService(authManager); // 3. Register tools directly on the server // Handle tool name normalization (dots to underscores) by default, or use dots if --use-dot-names is passed. @@ -1315,6 +1317,137 @@ System labels that can be modified: peopleService.getUserRelations, ); + // Tasks tools + server.registerTool( + 'tasks.listLists', + { + description: "Lists the authenticated user's task lists.", + inputSchema: { + maxResults: z + .number() + .optional() + .describe('Maximum number of task lists to return.'), + pageToken: z + .string() + .optional() + .describe('Token for the next page of results.'), + }, + ...readOnlyToolProps, + }, + tasksService.listTaskLists, + ); + + server.registerTool( + 'tasks.list', + { + description: 'Lists tasks in a specific task list.', + inputSchema: { + taskListId: z.string().describe('The ID of the task list.'), + showCompleted: z + .boolean() + .optional() + .describe('Whether to show completed tasks.'), + showDeleted: z + .boolean() + .optional() + .describe('Whether to show deleted tasks.'), + showHidden: z + .boolean() + .optional() + .describe('Whether to show hidden tasks.'), + showAssigned: z + .boolean() + .optional() + .describe('Whether to show tasks assigned from Docs or Chat.'), + maxResults: z + .number() + .optional() + .describe('Maximum number of tasks to return.'), + pageToken: z + .string() + .optional() + .describe('Token for the next page of results.'), + dueMin: z + .string() + .optional() + .describe( + "Lower bound for a task's due date (as a RFC 3339 timestamp).", + ), + dueMax: z + .string() + .optional() + .describe( + "Upper bound for a task's due date (as a RFC 3339 timestamp).", + ), + }, + ...readOnlyToolProps, + }, + tasksService.listTasks, + ); + + server.registerTool( + 'tasks.create', + { + description: 'Creates a new task in the specified task list.', + inputSchema: { + taskListId: z.string().describe('The ID of the task list.'), + title: z.string().describe('The title of the task.'), + notes: z.string().optional().describe('Notes for the task.'), + due: z + .string() + .optional() + .describe('The due date for the task (as a RFC 3339 timestamp).'), + }, + }, + tasksService.createTask, + ); + + server.registerTool( + 'tasks.update', + { + description: 'Updates an existing task.', + inputSchema: { + taskListId: z.string().describe('The ID of the task list.'), + taskId: z.string().describe('The ID of the task to update.'), + title: z.string().optional().describe('The new title of the task.'), + notes: z.string().optional().describe('The new notes for the task.'), + status: z + .enum(['needsAction', 'completed']) + .optional() + .describe('The new status of the task.'), + due: z + .string() + .optional() + .describe('The new due date for the task (as a RFC 3339 timestamp).'), + }, + }, + tasksService.updateTask, + ); + + server.registerTool( + 'tasks.complete', + { + description: 'Completes a task (convenience wrapper around update).', + inputSchema: { + taskListId: z.string().describe('The ID of the task list.'), + taskId: z.string().describe('The ID of the task to complete.'), + }, + }, + tasksService.completeTask, + ); + + server.registerTool( + 'tasks.delete', + { + description: 'Deletes a task.', + inputSchema: { + taskListId: z.string().describe('The ID of the task list.'), + taskId: z.string().describe('The ID of the task to delete.'), + }, + }, + tasksService.deleteTask, + ); + // 4. Connect the transport layer and start listening const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/workspace-server/src/services/TasksService.ts b/workspace-server/src/services/TasksService.ts new file mode 100644 index 0000000..c5c373d --- /dev/null +++ b/workspace-server/src/services/TasksService.ts @@ -0,0 +1,254 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { tasks_v1, google } from 'googleapis'; +import { AuthManager } from '../auth/AuthManager'; +import { logToFile } from '../utils/logger'; +import { gaxiosOptions } from '../utils/GaxiosConfig'; + +export class TasksService { + constructor(private authManager: AuthManager) {} + + private async getTasksClient(): Promise { + const auth = await this.authManager.getAuthenticatedClient(); + const options = { ...gaxiosOptions, auth }; + return google.tasks({ version: 'v1', ...options }); + } + + /** + * Lists the authenticated user's task lists. + */ + listTaskLists = async ( + params: { maxResults?: number; pageToken?: string } = {}, + ) => { + logToFile('Listing task lists'); + try { + const tasks = await this.getTasksClient(); + const response = await tasks.tasklists.list({ + maxResults: params.maxResults, + pageToken: params.pageToken, + }); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(response.data.items || []), + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + logToFile(`Error during tasks.listLists: ${errorMessage}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ error: errorMessage }), + }, + ], + }; + } + }; + + /** + * Lists tasks in a specific task list. + */ + listTasks = async (params: { + taskListId: string; + showCompleted?: boolean; + showDeleted?: boolean; + showHidden?: boolean; + showAssigned?: boolean; + maxResults?: number; + pageToken?: string; + dueMin?: string; + dueMax?: string; + }) => { + logToFile(`Listing tasks in list: ${params.taskListId}`); + try { + const tasks = await this.getTasksClient(); + const response = await tasks.tasks.list({ + tasklist: params.taskListId, + showCompleted: params.showCompleted, + showDeleted: params.showDeleted, + showHidden: params.showHidden, + showAssigned: params.showAssigned, + maxResults: params.maxResults, + pageToken: params.pageToken, + dueMin: params.dueMin, + dueMax: params.dueMax, + }); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(response.data.items || []), + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + logToFile(`Error during tasks.list: ${errorMessage}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ error: errorMessage }), + }, + ], + }; + } + }; + + /** + * Creates a new task in the specified task list. + */ + createTask = async (params: { + taskListId: string; + title: string; + notes?: string; + due?: string; + }) => { + logToFile(`Creating task in list: ${params.taskListId}`); + try { + const tasks = await this.getTasksClient(); + const requestBody: tasks_v1.Schema$Task = { + title: params.title, + ...(params.notes !== undefined && { notes: params.notes }), + ...(params.due !== undefined && { due: params.due }), + }; + + const response = await tasks.tasks.insert({ + tasklist: params.taskListId, + requestBody, + }); + + logToFile(`Successfully created task: ${response.data.id}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(response.data), + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + logToFile(`Error during tasks.create: ${errorMessage}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ error: errorMessage }), + }, + ], + }; + } + }; + + /** + * Updates an existing task. + */ + updateTask = async (params: { + taskListId: string; + taskId: string; + title?: string; + notes?: string; + status?: 'needsAction' | 'completed'; + due?: string; + }) => { + logToFile(`Updating task ${params.taskId} in list: ${params.taskListId}`); + try { + const tasks = await this.getTasksClient(); + const requestBody: tasks_v1.Schema$Task = { + ...(params.title !== undefined && { title: params.title }), + ...(params.notes !== undefined && { notes: params.notes }), + ...(params.status !== undefined && { status: params.status }), + ...(params.due !== undefined && { due: params.due }), + }; + + const response = await tasks.tasks.patch({ + tasklist: params.taskListId, + task: params.taskId, + requestBody, + }); + + logToFile(`Successfully updated task: ${params.taskId}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(response.data), + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + logToFile(`Error during tasks.update: ${errorMessage}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ error: errorMessage }), + }, + ], + }; + } + }; + + /** + * Completes a task (convenience wrapper around update). + */ + completeTask = async (params: { taskListId: string; taskId: string }) => { + return this.updateTask({ + taskListId: params.taskListId, + taskId: params.taskId, + status: 'completed', + }); + }; + + /** + * Deletes a task. + */ + deleteTask = async (params: { taskListId: string; taskId: string }) => { + logToFile(`Deleting task ${params.taskId} from list: ${params.taskListId}`); + try { + const tasks = await this.getTasksClient(); + await tasks.tasks.delete({ + tasklist: params.taskListId, + task: params.taskId, + }); + + logToFile(`Successfully deleted task: ${params.taskId}`); + return { + content: [ + { + type: 'text' as const, + text: `Task ${params.taskId} deleted successfully from list ${params.taskListId}.`, + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + logToFile(`Error during tasks.delete: ${errorMessage}`); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ error: errorMessage }), + }, + ], + }; + } + }; +} From 93d675169e459aa089cd8949976add248abd46b3 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Tue, 31 Mar 2026 00:37:40 +0000 Subject: [PATCH 2/3] fix(tasks): return full response data for pagination and use JSON for delete - listTaskLists and listTasks now return response.data instead of just items, preserving nextPageToken for pagination - deleteTask returns JSON message instead of plain text for consistency with other services --- .../src/__tests__/services/TasksService.test.ts | 12 ++++++------ workspace-server/src/services/TasksService.ts | 8 +++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/workspace-server/src/__tests__/services/TasksService.test.ts b/workspace-server/src/__tests__/services/TasksService.test.ts index 68bf6ef..d83d1cc 100644 --- a/workspace-server/src/__tests__/services/TasksService.test.ts +++ b/workspace-server/src/__tests__/services/TasksService.test.ts @@ -71,7 +71,7 @@ describe('TasksService', () => { maxResults: undefined, pageToken: undefined, }); - expect(JSON.parse(result.content[0].text)).toEqual(mockItems); + expect(JSON.parse(result.content[0].text)).toEqual({ items: mockItems }); }); it('should pass pagination parameters', async () => { @@ -94,7 +94,7 @@ describe('TasksService', () => { const result = await tasksService.listTaskLists(); - expect(JSON.parse(result.content[0].text)).toEqual([]); + expect(JSON.parse(result.content[0].text)).toEqual({}); }); it('should handle API errors gracefully', async () => { @@ -133,7 +133,7 @@ describe('TasksService', () => { dueMin: undefined, dueMax: undefined, }); - expect(JSON.parse(result.content[0].text)).toEqual(mockItems); + expect(JSON.parse(result.content[0].text)).toEqual({ items: mockItems }); }); it('should handle API errors gracefully', async () => { @@ -291,9 +291,9 @@ describe('TasksService', () => { tasklist: 'list1', task: 'task1', }); - expect(result.content[0].text).toBe( - 'Task task1 deleted successfully from list list1.', - ); + expect(JSON.parse(result.content[0].text)).toEqual({ + message: 'Task task1 deleted successfully from list list1.', + }); }); it('should handle API errors gracefully', async () => { diff --git a/workspace-server/src/services/TasksService.ts b/workspace-server/src/services/TasksService.ts index c5c373d..3cbafc7 100644 --- a/workspace-server/src/services/TasksService.ts +++ b/workspace-server/src/services/TasksService.ts @@ -36,7 +36,7 @@ export class TasksService { content: [ { type: 'text' as const, - text: JSON.stringify(response.data.items || []), + text: JSON.stringify(response.data), }, ], }; @@ -88,7 +88,7 @@ export class TasksService { content: [ { type: 'text' as const, - text: JSON.stringify(response.data.items || []), + text: JSON.stringify(response.data), }, ], }; @@ -233,7 +233,9 @@ export class TasksService { content: [ { type: 'text' as const, - text: `Task ${params.taskId} deleted successfully from list ${params.taskListId}.`, + text: JSON.stringify({ + message: `Task ${params.taskId} deleted successfully from list ${params.taskListId}.`, + }), }, ], }; From cdfc646586e394f3ce13481893d28c6fd969df4c Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Tue, 31 Mar 2026 17:16:03 +0000 Subject: [PATCH 3/3] fix(tasks): only include defined params in API calls Use conditional spread to omit undefined parameters from tasklists.list and tasks.list calls, consistent with createTask and updateTask. --- .../__tests__/services/TasksService.test.ts | 12 +------ workspace-server/src/services/TasksService.ts | 36 +++++++++++++------ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/workspace-server/src/__tests__/services/TasksService.test.ts b/workspace-server/src/__tests__/services/TasksService.test.ts index d83d1cc..2584a8c 100644 --- a/workspace-server/src/__tests__/services/TasksService.test.ts +++ b/workspace-server/src/__tests__/services/TasksService.test.ts @@ -67,10 +67,7 @@ describe('TasksService', () => { const result = await tasksService.listTaskLists(); - expect(mockTasksAPI.tasklists.list).toHaveBeenCalledWith({ - maxResults: undefined, - pageToken: undefined, - }); + expect(mockTasksAPI.tasklists.list).toHaveBeenCalledWith({}); expect(JSON.parse(result.content[0].text)).toEqual({ items: mockItems }); }); @@ -124,14 +121,7 @@ describe('TasksService', () => { expect(mockTasksAPI.tasks.list).toHaveBeenCalledWith({ tasklist: 'list1', - showCompleted: undefined, - showDeleted: undefined, - showHidden: undefined, showAssigned: true, - maxResults: undefined, - pageToken: undefined, - dueMin: undefined, - dueMax: undefined, }); expect(JSON.parse(result.content[0].text)).toEqual({ items: mockItems }); }); diff --git a/workspace-server/src/services/TasksService.ts b/workspace-server/src/services/TasksService.ts index 3cbafc7..2e9cb7d 100644 --- a/workspace-server/src/services/TasksService.ts +++ b/workspace-server/src/services/TasksService.ts @@ -28,8 +28,12 @@ export class TasksService { try { const tasks = await this.getTasksClient(); const response = await tasks.tasklists.list({ - maxResults: params.maxResults, - pageToken: params.pageToken, + ...(params.maxResults !== undefined && { + maxResults: params.maxResults, + }), + ...(params.pageToken !== undefined && { + pageToken: params.pageToken, + }), }); return { @@ -74,14 +78,26 @@ export class TasksService { const tasks = await this.getTasksClient(); const response = await tasks.tasks.list({ tasklist: params.taskListId, - showCompleted: params.showCompleted, - showDeleted: params.showDeleted, - showHidden: params.showHidden, - showAssigned: params.showAssigned, - maxResults: params.maxResults, - pageToken: params.pageToken, - dueMin: params.dueMin, - dueMax: params.dueMax, + ...(params.showCompleted !== undefined && { + showCompleted: params.showCompleted, + }), + ...(params.showDeleted !== undefined && { + showDeleted: params.showDeleted, + }), + ...(params.showHidden !== undefined && { + showHidden: params.showHidden, + }), + ...(params.showAssigned !== undefined && { + showAssigned: params.showAssigned, + }), + ...(params.maxResults !== undefined && { + maxResults: params.maxResults, + }), + ...(params.pageToken !== undefined && { + pageToken: params.pageToken, + }), + ...(params.dueMin !== undefined && { dueMin: params.dueMin }), + ...(params.dueMax !== undefined && { dueMax: params.dueMax }), }); return {