Skip to content

Commit e776475

Browse files
committed
fix(search): isolate unavailable Calendar accounts
1 parent bad0ce4 commit e776475

3 files changed

Lines changed: 88 additions & 17 deletions

File tree

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -502,21 +502,24 @@ describe('Google Workspace per-user central crawl', () => {
502502
expect((await list(context(), undefined, CONFIG, 'google_calendar')).documents).toHaveLength(1)
503503
})
504504

505-
it('isolates explicit Calendar list access failures without claiming a disabled service', async () => {
506-
listUserDocuments.mockRejectedValueOnce(
507-
new GoogleApiError('calendar.events.list', 403, ['forbidden'])
508-
)
509-
const first = await list(context(), undefined, CONFIG, 'google_calendar')
510-
expect(first.listingFailures?.samples[0]).toEqual({
511-
scope: 'alice@corp.com',
512-
operation: 'calendar.events.list',
513-
status: 403,
514-
reasons: ['forbidden'],
515-
})
516-
const second = await list(context(), first.nextCursor, CONFIG, 'google_calendar')
517-
expect(second.documents[0].acl).toEqual(['u:bob@corp.com'])
518-
expect(second.reconciliationSafe).toBe(false)
519-
})
505+
it.each(['forbidden', 'notACalendarUser'])(
506+
'isolates explicit Calendar list access failures (%s) without claiming a disabled service',
507+
async (reason) => {
508+
listUserDocuments.mockRejectedValueOnce(
509+
new GoogleApiError('calendar.events.list', 403, [reason])
510+
)
511+
const first = await list(context(), undefined, CONFIG, 'google_calendar')
512+
expect(first.listingFailures?.samples[0]).toEqual({
513+
scope: 'alice@corp.com',
514+
operation: 'calendar.events.list',
515+
status: 403,
516+
reasons: [reason],
517+
})
518+
const second = await list(context(), first.nextCursor, CONFIG, 'google_calendar')
519+
expect(second.documents[0].acl).toEqual(['u:bob@corp.com'])
520+
expect(second.reconciliationSafe).toBe(false)
521+
}
522+
)
520523

521524
it.each([{ error: { code: 403 } }, { error: { code: 403, errors: [], details: [] } }])(
522525
'propagates a Calendar 403 without reason codes: %j',
@@ -542,6 +545,9 @@ describe('Google Workspace per-user central crawl', () => {
542545
[403, ['domainPolicy']],
543546
[403, ['unrecognized-provider-code']],
544547
[403, ['forbidden', 'unrecognized-provider-code']],
548+
[403, ['notACalendarUser', 'unrecognized-provider-code']],
549+
[403, ['notACalendarUser', 'insufficientPermissions']],
550+
[403, ['notACalendarUser', 'rateLimitExceeded']],
545551
[401, ['authError']],
546552
[429, []],
547553
[500, ['backendError']],
@@ -571,6 +577,15 @@ describe('Google Workspace per-user central crawl', () => {
571577
await expect(list(context(), undefined, CONFIG, 'google_calendar')).rejects.toBe(error)
572578
})
573579

580+
it.each([
581+
new GoogleApiError('calendar.events.list', 403, ['notACalendarUser'], false),
582+
new GoogleApiError('calendar.calendarList.list', 403, ['notACalendarUser']),
583+
new GoogleApiError('calendar.events.list', 401, ['notACalendarUser']),
584+
])('does not isolate Calendar unavailability outside a complete list 403: %s', async (error) => {
585+
listUserDocuments.mockRejectedValueOnce(error)
586+
await expect(list(context(), undefined, CONFIG, 'google_calendar')).rejects.toBe(error)
587+
})
588+
574589
it('does not suppress delegation failures that resemble provider list failures', async () => {
575590
const ctx = context()
576591
const error = new GoogleApiError('gmail.threads.list', 400, ['failedPrecondition'])

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ function userListingFailure(
177177
: error.diagnostic.operation === 'calendar.events.list' &&
178178
error.status === 403 &&
179179
reasons.length > 0 &&
180-
reasons.every((reason) => reason === 'forbidden')
180+
reasons.every((reason) => reason === 'forbidden' || reason === 'notACalendarUser')
181181
return isolated
182182
? { operation: error.diagnostic.operation, status: error.status, reasons: [...reasons] }
183183
: null

‎apps/sim/lib/knowledge/connectors/google-company-scheduler.test.ts‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@ import type {
1515
} from '@/lib/knowledge/connectors/partition-work'
1616
import { GoogleDriveApiError } from '@/connectors/google-drive/google-drive-errors'
1717
import { GoogleApiError } from '@/connectors/google-workspace/api-errors'
18+
import { listGoogleWorkspaceDocuments } from '@/connectors/google-workspace/company-crawl'
1819
import { googleCompanyUserContextSchema } from '@/connectors/google-workspace/company-work'
1920
import type { GoogleWorkspaceUser } from '@/connectors/google-workspace/users'
2021
import { ConnectorSourceError } from '@/connectors/source-error'
2122
import type { ConnectorConfig, ExternalDocument, ExternalListingFailures } from '@/connectors/types'
23+
import { memberDocumentId } from '@/connectors/utils'
2224

23-
const mocks = vi.hoisted(() => ({ directory: vi.fn() }))
25+
const mocks = vi.hoisted(() => ({ directory: vi.fn(), getUser: vi.fn() }))
2426
vi.mock('@/connectors/google-workspace/users', async (original) => ({
2527
...(await original<typeof import('@/connectors/google-workspace/users')>()),
2628
listGoogleWorkspaceUsers: mocks.directory,
29+
getGoogleWorkspaceUser: mocks.getUser,
2730
}))
2831

2932
const document: ExternalDocument = {
@@ -203,6 +206,7 @@ function fixture(provider = 'google_calendar', syncIntervalMinutes = 60) {
203206

204207
beforeEach(() => {
205208
mocks.directory.mockReset()
209+
mocks.getUser.mockReset().mockImplementation(async (_token: string, id: string) => user(id))
206210
})
207211

208212
describe('durable Google company user scheduling', () => {
@@ -269,6 +273,58 @@ describe('durable Google company user scheduling', () => {
269273
}
270274
)
271275

276+
it('continues past unavailable Calendar users without the unresolved-error pause and retries them later', async () => {
277+
mocks.directory.mockResolvedValue({ users: ['a', 'b', 'c', 'z'].map(user) })
278+
const f = fixture()
279+
const listUserDocuments = vi.fn<ConnectorConfig['listDocuments']>(
280+
async (_token, _config, _cursor, ctx) => ({
281+
documents: [{ ...document, externalId: memberDocumentId('event', ctx) }],
282+
hasMore: false,
283+
})
284+
)
285+
for (let i = 0; i < 3; i++) {
286+
listUserDocuments.mockRejectedValueOnce(
287+
new GoogleApiError('calendar.events.list', 403, ['notACalendarUser'])
288+
)
289+
}
290+
const syncContext = {
291+
mirrorsSourceAcls: true,
292+
getDelegatedAccessToken: vi.fn(async () => 'user-token'),
293+
}
294+
f.list.mockImplementation(async (accessToken, sourceConfig, cursor) =>
295+
listGoogleWorkspaceDocuments({
296+
provider: 'google_calendar',
297+
accessToken,
298+
sourceConfig,
299+
cursor,
300+
syncContext,
301+
listUserDocuments,
302+
})
303+
)
304+
305+
await f.step(5)
306+
307+
expect(f.rows.get('z:content')?.complete).toBe(true)
308+
expect(f.saved()).toMatchObject({ complete: false, unsafe: true, resumeAt: null })
309+
for (const id of ['a', 'b', 'c']) {
310+
expect(f.rows.get(`${id}:content`)).toMatchObject({
311+
complete: false,
312+
attempts: 1,
313+
retryAt: new Date('2026-09-17T01:00:00Z'),
314+
failure: { status: 403, reasons: ['notACalendarUser'] },
315+
})
316+
}
317+
318+
f.advance(60 * 60 * 1000)
319+
f.restart()
320+
await f.step(4)
321+
322+
for (const id of ['a', 'b', 'c']) {
323+
expect(f.rows.get(`${id}:content`)).toMatchObject({ complete: true, attempts: 0 })
324+
expect(f.rows.get(`${id}:content`)?.failure).toBeUndefined()
325+
}
326+
})
327+
272328
it('bounds a run of unresolved user errors rather than marking the tenant complete', async () => {
273329
mocks.directory.mockResolvedValue({ users: ['a', 'b', 'c', 'd'].map(user) })
274330
const f = fixture()

0 commit comments

Comments
 (0)