Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 50 additions & 13 deletions workspace-server/src/__tests__/services/CalendarService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ describe('CalendarService', () => {
attendees: [{ email: 'new@example.com' }],
};

mockCalendarAPI.events.update.mockResolvedValue({ data: updatedEvent });
mockCalendarAPI.events.patch.mockResolvedValue({ data: updatedEvent });

const result = await calendarService.updateEvent({
eventId: 'event123',
Expand All @@ -867,7 +867,7 @@ describe('CalendarService', () => {
attendees: ['new@example.com'],
});

expect(mockCalendarAPI.events.update).toHaveBeenCalledWith({
expect(mockCalendarAPI.events.patch).toHaveBeenCalledWith({
calendarId: 'primary',
eventId: 'event123',
requestBody: {
Expand All @@ -889,14 +889,14 @@ describe('CalendarService', () => {
description: 'New updated description',
};

mockCalendarAPI.events.update.mockResolvedValue({ data: updatedEvent });
mockCalendarAPI.events.patch.mockResolvedValue({ data: updatedEvent });

const result = await calendarService.updateEvent({
eventId: 'event123',
description: 'New updated description',
});

expect(mockCalendarAPI.events.update).toHaveBeenCalledWith({
expect(mockCalendarAPI.events.patch).toHaveBeenCalledWith({
calendarId: 'primary',
eventId: 'event123',
requestBody: {
Expand All @@ -910,7 +910,7 @@ describe('CalendarService', () => {

it('should handle update errors', async () => {
const apiError = new Error('Update failed');
mockCalendarAPI.events.update.mockRejectedValue(apiError);
mockCalendarAPI.events.patch.mockRejectedValue(apiError);

const result = await calendarService.updateEvent({
eventId: 'event123',
Expand All @@ -927,21 +927,58 @@ describe('CalendarService', () => {
summary: 'Updated Meeting Only',
};

mockCalendarAPI.events.update.mockResolvedValue({ data: updatedEvent });
mockCalendarAPI.events.patch.mockResolvedValue({ data: updatedEvent });

await calendarService.updateEvent({
eventId: 'event123',
summary: 'Updated Meeting Only',
});

expect(mockCalendarAPI.events.update).toHaveBeenCalledWith({
expect(mockCalendarAPI.events.patch).toHaveBeenCalledWith({
calendarId: 'primary',
eventId: 'event123',
requestBody: {
summary: 'Updated Meeting Only',
},
});
});

it('should preserve Focus Time eventType on partial update', async () => {
const focusTimeEvent = {
id: 'focus123',
summary: 'Deep Work',
eventType: 'focusTime',
focusTimeProperties: { autoDeclineMode: 'declineNone' },
start: { dateTime: '2024-01-15T10:00:00Z' },
end: { dateTime: '2024-01-15T12:00:00Z' },
};

mockCalendarAPI.events.patch.mockResolvedValue({ data: focusTimeEvent });

const result = await calendarService.updateEvent({
eventId: 'focus123',
start: { dateTime: '2024-01-16T10:00:00Z' },
end: { dateTime: '2024-01-16T12:00:00Z' },
});

// Verify patch was called (not update) — patch preserves unspecified fields
expect(mockCalendarAPI.events.patch).toHaveBeenCalled();
expect(mockCalendarAPI.events.update).not.toHaveBeenCalled();

// Verify only the requested fields were sent (eventType is NOT in the body —
// it is preserved server-side by PATCH semantics)
expect(mockCalendarAPI.events.patch).toHaveBeenCalledWith(
expect.objectContaining({
requestBody: {
start: { dateTime: '2024-01-16T10:00:00Z' },
end: { dateTime: '2024-01-16T12:00:00Z' },
},
}),
);

const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.eventType).toBe('focusTime');
});
});

describe('respondToEvent', () => {
Expand Down Expand Up @@ -1495,14 +1532,14 @@ describe('CalendarService', () => {
},
};

mockCalendarAPI.events.update.mockResolvedValue({ data: updatedEvent });
mockCalendarAPI.events.patch.mockResolvedValue({ data: updatedEvent });

const result = await calendarService.updateEvent({
eventId: 'event123',
addGoogleMeet: true,
});

const callArgs = mockCalendarAPI.events.update.mock.calls[0][0];
const callArgs = mockCalendarAPI.events.patch.mock.calls[0][0];
expect(callArgs.conferenceDataVersion).toBe(1);
expect(callArgs.requestBody.conferenceData).toBeDefined();
expect(
Expand All @@ -1515,15 +1552,15 @@ describe('CalendarService', () => {

it('should not include conferenceData when addGoogleMeet is false', async () => {
const updatedEvent = { id: 'event123', summary: 'No Meet' };
mockCalendarAPI.events.update.mockResolvedValue({ data: updatedEvent });
mockCalendarAPI.events.patch.mockResolvedValue({ data: updatedEvent });

await calendarService.updateEvent({
eventId: 'event123',
summary: 'No Meet',
addGoogleMeet: false,
});

const callArgs = mockCalendarAPI.events.update.mock.calls[0][0];
const callArgs = mockCalendarAPI.events.patch.mock.calls[0][0];
expect(callArgs.conferenceDataVersion).toBeUndefined();
expect(callArgs.requestBody.conferenceData).toBeUndefined();
});
Expand All @@ -1541,7 +1578,7 @@ describe('CalendarService', () => {
],
};

mockCalendarAPI.events.update.mockResolvedValue({ data: updatedEvent });
mockCalendarAPI.events.patch.mockResolvedValue({ data: updatedEvent });

const result = await calendarService.updateEvent({
eventId: 'event123',
Expand All @@ -1553,7 +1590,7 @@ describe('CalendarService', () => {
],
});

const callArgs = mockCalendarAPI.events.update.mock.calls[0][0];
const callArgs = mockCalendarAPI.events.patch.mock.calls[0][0];
expect(callArgs.supportsAttachments).toBe(true);
expect(callArgs.requestBody.attachments).toEqual([
expect.objectContaining({
Expand Down
13 changes: 9 additions & 4 deletions workspace-server/src/services/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ export class CalendarService {
try {
const calendar = await this.getCalendar();

// Build request body with only the fields to update (patch semantics)
// Build request body with only the fields to update (true patch semantics)
const requestBody: calendar_v3.Schema$Event = {};
if (summary !== undefined) requestBody.summary = summary;
if (description !== undefined) requestBody.description = description;
Expand All @@ -733,19 +733,24 @@ export class CalendarService {
if (attendees)
requestBody.attendees = attendees.map((email) => ({ email }));

const updateParams: calendar_v3.Params$Resource$Events$Update = {
const patchParams: calendar_v3.Params$Resource$Events$Patch = {
calendarId: finalCalendarId,
eventId,
requestBody,
};
this.applyMeetAndAttachments(
requestBody,
updateParams,
patchParams,
addGoogleMeet,
attachments,
);

const res = await calendar.events.update(updateParams);
// Use events.patch (not events.update) so only specified fields are modified.
// events.update (PUT) replaces the entire event, which wipes unspecified fields
// like summary, description, reminders, colorId, visibility, and — critically —
// rejects non-default eventTypes (Focus Time, Out of Office, Working Location)
// with "Event type cannot be changed."
const res = await calendar.events.patch(patchParams);

logToFile(`Successfully updated event: ${res.data.id}`);
return {
Expand Down