Skip to content

Commit 22e02f0

Browse files
committed
fix(google-calendar): honor cancellation on RSVP write and tighten outputs
1 parent dbbd1ee commit 22e02f0

6 files changed

Lines changed: 48 additions & 15 deletions

File tree

‎apps/docs/content/docs/integrations/google_calendar.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ RSVP to a Google Calendar event (accept, decline, or tentative) as the connected
320320
| Parameter | Type | Required | Description |
321321
| --------- | ---- | -------- | ----------- |
322322
| `calendarId` | string | No | Google Calendar ID the invitation appears on \(e.g., primary or calendar@group.calendar.google.com\) |
323-
| `eventId` | string | Yes | Google Calendar event ID to respond to. Use a recurring-event instance ID \(from Get Recurring Instances\) to respond to a single occurrence; the series ID responds to every occurrence. |
323+
| `eventId` | string | Yes | Google Calendar event ID to respond to. Use a recurring-event instance ID \(as returned by List Events or Get Recurring Instances\) to respond to a single occurrence; the series ID responds to every occurrence. |
324324
| `responseStatus` | string | Yes | Your response: accepted, declined, or tentative |
325325
| `comment` | string | No | Optional note to include with your response |
326326
| `sendUpdates` | string | No | Who to notify about your response: all, externalOnly, or none |

‎apps/sim/blocks/blocks/google_calendar.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1242,7 +1242,7 @@ export const GoogleCalendarBlockMeta = {
12421242
name: 'rsvp-to-event',
12431243
description: 'Accept, decline, or tentatively accept a Google Calendar invitation.',
12441244
content:
1245-
'# RSVP to an Event\n\nChange your own response to an invitation without touching the guest list.\n\n## Steps\n1. Find the event with List Events over the right time window. List Events returns each occurrence of a recurring meeting with its own ID, so use that ID to respond to one occurrence only; the series ID (recurringEventId) changes your response for every occurrence.\n2. Run Respond to Invitation (RSVP) with Response set to `Yes (accept)`, `No (decline)`, or `Maybe (tentative)`, and an optional note.\n3. Do not use Update Event or Invite Attendees to RSVP; they edit the guest list, not your response.\n\n## Output\nConfirm the event title, the occurrence start time, and the confirmed response returned by the tool.',
1245+
'# RSVP to an Event\n\nChange your own response to an invitation without touching the guest list.\n\n## Steps\n1. Find the event with List Events over the right time window. List Events returns each occurrence of a recurring meeting with its own ID, so responding with that ID changes only that occurrence.\n2. Run Respond to Invitation (RSVP) with Response set to `Yes (accept)`, `No (decline)`, or `Maybe (tentative)`, and an optional note.\n3. Do not use Update Event or Invite Attendees to RSVP; they edit the guest list, not your response.\n\n## Output\nConfirm the event title, the occurrence start time, and the confirmed response returned by the tool.',
12461246
},
12471247
],
12481248
} as const satisfies BlockMeta

‎apps/sim/tools/generated/tool-metadata.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/generated/tool-outputs.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/google_calendar/respond.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,31 @@ describe('google_calendar_respond', () => {
129129
).rejects.toThrow('Forbidden')
130130
})
131131

132+
it('never writes the RSVP when the run was canceled', async () => {
133+
const fetchMock = vi.fn()
134+
vi.stubGlobal('fetch', fetchMock)
135+
const controller = new AbortController()
136+
controller.abort()
137+
138+
await expect(
139+
respondTool.transformResponse!(jsonResponse(eventWith('needsAction')), baseParams, {
140+
signal: controller.signal,
141+
})
142+
).rejects.toThrow()
143+
expect(fetchMock).not.toHaveBeenCalled()
144+
})
145+
146+
it('forwards the execution signal to the RSVP write', async () => {
147+
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(eventWith('accepted')))
148+
vi.stubGlobal('fetch', fetchMock)
149+
const controller = new AbortController()
150+
151+
await respondTool.transformResponse!(jsonResponse(eventWith('needsAction')), baseParams, {
152+
signal: controller.signal,
153+
})
154+
expect(fetchMock.mock.calls[0][1].signal).toBe(controller.signal)
155+
})
156+
132157
it('fails when Google does not reflect the new response', async () => {
133158
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse(eventWith('needsAction'))))
134159

‎apps/sim/tools/google_calendar/respond.ts‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
type GoogleCalendarRespondParams,
66
type GoogleCalendarRespondResponse,
77
} from '@/tools/google_calendar/types'
8-
import type { ToolConfig } from '@/tools/types'
8+
import type { ToolConfig, ToolResponseContext } from '@/tools/types'
99

1010
const RESPONSE_STATUSES = ['accepted', 'declined', 'tentative'] as const
1111

@@ -20,10 +20,12 @@ interface RespondResult {
2020
* entry flagged `self`, then patched with `attendeesOmitted: true` and only that entry, which
2121
* tells Google to update just this participant's response and leave every other guest intact.
2222
* Passing a recurring-event instance ID changes the response for that occurrence only.
23+
* The execution signal is forwarded so a canceled run never writes the RSVP.
2324
*/
2425
async function respondToEvent(
2526
response: Response,
26-
params: GoogleCalendarRespondParams | undefined
27+
params: GoogleCalendarRespondParams | undefined,
28+
signal: AbortSignal | undefined
2729
): Promise<RespondResult> {
2830
const existingEvent: GoogleCalendarApiEventResponse = await response.json()
2931

@@ -61,8 +63,10 @@ async function respondToEvent(
6163
const queryString = queryParams.toString()
6264
const patchUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId?.trim() ?? '')}${queryString ? `?${queryString}` : ''}`
6365

66+
signal?.throwIfAborted()
6467
const patchResponse = await fetch(patchUrl, {
6568
method: 'PATCH',
69+
signal,
6670
headers: {
6771
Authorization: `Bearer ${params?.accessToken}`,
6872
'Content-Type': 'application/json',
@@ -117,7 +121,7 @@ export const respondTool: ToolConfig<GoogleCalendarRespondParams, GoogleCalendar
117121
required: true,
118122
visibility: 'user-or-llm',
119123
description:
120-
'Google Calendar event ID to respond to. Use a recurring-event instance ID (from Get Recurring Instances) to respond to a single occurrence; the series ID responds to every occurrence.',
124+
'Google Calendar event ID to respond to. Use a recurring-event instance ID (as returned by List Events or Get Recurring Instances) to respond to a single occurrence; the series ID responds to every occurrence.',
121125
},
122126
responseStatus: {
123127
type: 'string',
@@ -151,8 +155,8 @@ export const respondTool: ToolConfig<GoogleCalendarRespondParams, GoogleCalendar
151155
}),
152156
},
153157

154-
transformResponse: async (response: Response, params) => {
155-
const { data, responseStatus } = await respondToEvent(response, params)
158+
transformResponse: async (response: Response, params, context?: ToolResponseContext) => {
159+
const { data, responseStatus } = await respondToEvent(response, params, context?.signal)
156160

157161
return {
158162
success: true,
@@ -212,8 +216,12 @@ export const respondV2Tool: ToolConfig<
212216
oauth: respondTool.oauth,
213217
params: respondTool.params,
214218
request: respondTool.request,
215-
transformResponse: async (response: Response, params) => {
216-
const { data, responseStatus, comment } = await respondToEvent(response, params)
219+
transformResponse: async (response: Response, params, context?: ToolResponseContext) => {
220+
const { data, responseStatus, comment } = await respondToEvent(
221+
response,
222+
params,
223+
context?.signal
224+
)
217225

218226
return {
219227
success: true,
@@ -235,15 +243,15 @@ export const respondV2Tool: ToolConfig<
235243
id: { type: 'string', description: 'Event ID' },
236244
htmlLink: { type: 'string', description: 'Event link' },
237245
status: { type: 'string', description: 'Event status' },
238-
summary: { type: 'string', description: 'Event title', optional: true },
246+
summary: { type: 'string', description: 'Event title', nullable: true },
239247
start: { type: 'json', description: 'Event start' },
240248
end: { type: 'json', description: 'Event end' },
241249
responseStatus: {
242250
type: 'string',
243251
description: 'Your confirmed response (accepted, declined, or tentative)',
244252
},
245-
comment: { type: 'string', description: 'Your response comment', optional: true },
246-
attendees: { type: 'json', description: 'Event attendees', optional: true },
247-
organizer: { type: 'json', description: 'Event organizer', optional: true },
253+
comment: { type: 'string', description: 'Your response comment', nullable: true },
254+
attendees: { type: 'json', description: 'Event attendees', nullable: true },
255+
organizer: { type: 'json', description: 'Event organizer', nullable: true },
248256
},
249257
}

0 commit comments

Comments
 (0)