Skip to content

Commit 2279bf5

Browse files
committed
fix(search): require explicit Calendar access failure reasons
1 parent 1a59ca1 commit 2279bf5

3 files changed

Lines changed: 30 additions & 18 deletions

File tree

‎apps/docs/content/docs/search/google-calendar.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Sim indexes event titles, descriptions, times, locations, and the selected atten
131131

132132
Cancelled events, attachment contents, meeting recordings, and transcripts are not indexed. Status entries such as working location, out of office, focus time, and birthdays, and automatically generated reservation events from Gmail are not indexed. Events Google returns only as free/busy blocks, without searchable details, are not indexed. Events outside the selected date window are excluded. Private event details that Google withholds are not available in Search; see [Google's calendar sharing rules](https://developers.google.com/workspace/calendar/api/concepts/sharing).
133133

134-
Search schedules syncs hourly. Event edits, cancellations, access changes, inactive or removed users, and events moving outside the date window are reconciled during completed background syncs. Central crawls page through each selected user and resume unfinished work before removing documents no longer listed. If an individual user's event listing returns a `403` with no reason or only `forbidden`, Sim records a warning and continues with the remaining users. The crawl stays incomplete and retries affected users on the next scheduled crawl; unread calendars are not treated as empty. Credential, delegation, Directory, and other provider failures still stop the crawl. The first sync may take longer, and results appear as indexing progresses; Search is not a live Calendar read.
134+
Search schedules syncs hourly. Event edits, cancellations, access changes, inactive or removed users, and events moving outside the date window are reconciled during completed background syncs. Central crawls page through each selected user and resume unfinished work before removing documents no longer listed. If an individual user's event listing returns a `403` with an explicit `forbidden` reason and no other reasons, Sim records a warning and continues with the remaining users. The crawl stays incomplete and retries affected users on the next scheduled crawl; unread calendars are not treated as empty. A `403` without a reason stops the crawl because its cause is unknown. Credential, delegation, Directory, and other provider failures still stop the crawl. The first sync may take longer, and results appear as indexing progresses; Search is not a live Calendar read.
135135

136136
## Troubleshooting
137137

‎apps/sim/connectors/google-workspace/company-crawl.test.ts‎

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @vitest-environment node */
22
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3-
import { GoogleApiError } from '@/connectors/google-workspace/api-errors'
3+
import { GoogleApiError, readGoogleApiError } from '@/connectors/google-workspace/api-errors'
44
import {
55
getGoogleWorkspaceDocument,
66
InvalidGoogleWorkspaceCursor,
@@ -501,26 +501,37 @@ describe('Google Workspace per-user central crawl', () => {
501501
expect((await list(context(), undefined, CONFIG, 'google_calendar')).documents).toHaveLength(1)
502502
})
503503

504-
it.each([{ reasons: [] }, { reasons: ['forbidden'] }])(
505-
'isolates Calendar list access failures without claiming a disabled service (%j)',
506-
async ({ reasons }) => {
507-
listUserDocuments.mockRejectedValueOnce(
508-
new GoogleApiError('calendar.events.list', 403, reasons)
509-
)
510-
const first = await list(context(), undefined, CONFIG, 'google_calendar')
511-
expect(first.listingFailures?.samples[0]).toEqual({
512-
scope: 'alice@corp.com',
513-
operation: 'calendar.events.list',
514-
status: 403,
515-
reasons,
516-
})
517-
const second = await list(context(), first.nextCursor, CONFIG, 'google_calendar')
518-
expect(second.documents[0].acl).toEqual(['u:bob@corp.com'])
519-
expect(second.reconciliationSafe).toBe(false)
504+
it('isolates explicit Calendar list access failures without claiming a disabled service', async () => {
505+
listUserDocuments.mockRejectedValueOnce(
506+
new GoogleApiError('calendar.events.list', 403, ['forbidden'])
507+
)
508+
const first = await list(context(), undefined, CONFIG, 'google_calendar')
509+
expect(first.listingFailures?.samples[0]).toEqual({
510+
scope: 'alice@corp.com',
511+
operation: 'calendar.events.list',
512+
status: 403,
513+
reasons: ['forbidden'],
514+
})
515+
const second = await list(context(), first.nextCursor, CONFIG, 'google_calendar')
516+
expect(second.documents[0].acl).toEqual(['u:bob@corp.com'])
517+
expect(second.reconciliationSafe).toBe(false)
518+
})
519+
520+
it.each([{ error: { code: 403 } }, { error: { code: 403, errors: [], details: [] } }])(
521+
'propagates a Calendar 403 without reason codes: %j',
522+
async (body) => {
523+
const error = await readGoogleApiError(json(body, 403), 'calendar.events.list')
524+
listUserDocuments.mockRejectedValueOnce(error)
525+
const ctx: Record<string, unknown> = context()
526+
527+
await expect(list(ctx, undefined, CONFIG, 'google_calendar')).rejects.toBe(error)
528+
expect(ctx.reconciliationUnsafe).toBeUndefined()
529+
expect(listUserDocuments).toHaveBeenCalledOnce()
520530
}
521531
)
522532

523533
it.each([
534+
[403, []],
524535
[403, ['rateLimitExceeded']],
525536
[403, ['userRateLimitExceeded']],
526537
[403, ['quotaExceeded']],

‎apps/sim/connectors/google-workspace/company-crawl.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ function userListingFailure(
153153
reasons.every((reason) => reason === 'failedPrecondition')
154154
: error.diagnostic.operation === 'calendar.events.list' &&
155155
error.status === 403 &&
156+
reasons.length > 0 &&
156157
reasons.every((reason) => reason === 'forbidden')
157158
return isolated
158159
? { operation: error.diagnostic.operation, status: error.status, reasons: [...reasons] }

0 commit comments

Comments
 (0)