Skip to content

Commit 8cff97e

Browse files
committed
fix(search): harden crawl completion and recovery
1 parent c3e8f1d commit 8cff97e

10 files changed

Lines changed: 508 additions & 58 deletions

apps/sim/lib/knowledge/__integration__/google-company-work.integration.ts

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import {
77
user,
88
workspace,
99
} from '@sim/db/schema'
10-
import { eq, inArray } from 'drizzle-orm'
11-
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
10+
import { and, eq, inArray } from 'drizzle-orm'
11+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1212
import { seedKnowledgeAclFixture } from '@/lib/knowledge/__integration__/seed-source-access-fixture'
13+
import type { GoogleCompanyWorkUpdate } from '@/lib/knowledge/connectors/google-company-scheduler'
1314
import {
1415
commitGoogleCompanyWork,
1516
googleCompanyWorkStore,
@@ -35,7 +36,7 @@ describe('Google company user checkpoint storage', () => {
3536
startedAt: generationStartedAt,
3637
})
3738

38-
beforeAll(async () => {
39+
beforeEach(async () => {
3940
owner = await seedKnowledgeAclFixture(undefined, { connectorType: 'google_drive' })
4041
await db.transaction(async (tx) => {
4142
await commitGoogleCompanyWork(
@@ -51,12 +52,13 @@ describe('Google company user checkpoint storage', () => {
5152
.where(eq(knowledgeConnector.id, owner.connectorId))
5253
})
5354
})
54-
afterAll(async () => {
55+
afterEach(async () => {
56+
vi.unstubAllEnvs()
5557
await db.delete(workspace).where(eq(workspace.id, owner.workspaceId))
5658
await db.delete(organization).where(eq(organization.id, owner.organizationId))
5759
await db.delete(user).where(inArray(user.id, [owner.aliceId, owner.bobId]))
58-
await db.$client.end()
5960
})
61+
afterAll(() => db.$client.end())
6062

6163
it('rolls back user progress and the owning checkpoint together', async () => {
6264
const store = googleCompanyWorkStore(owner.connectorId, generationId)
@@ -208,6 +210,23 @@ describe('Google company user checkpoint storage', () => {
208210
})
209211

210212
it('does not rescan completed manual work or use its past retry time to wake blocked work', async () => {
213+
await commitGoogleCompanyWork(
214+
db,
215+
owner.connectorId,
216+
generationId,
217+
{
218+
update: {
219+
userId: 'first',
220+
kind: 'content',
221+
cursor: 'blocked-user-page',
222+
completed: false,
223+
retryAt: new Date(Date.now() + 60 * 60_000),
224+
attempts: 1,
225+
failure: { scope: first.user.email, operation: 'drive.files.list', reasons: [] },
226+
},
227+
},
228+
generationStartedAt
229+
)
211230
await commitGoogleCompanyWork(
212231
db,
213232
owner.connectorId,
@@ -252,7 +271,80 @@ describe('Google company user checkpoint storage', () => {
252271
expect((await automatic.remaining()).count).toBe(0)
253272
})
254273

255-
it('resets both provider continuations on a new configuration generation and cascades on connector deletion', async () => {
274+
it.each(['continuation', 'failure'] as const)(
275+
'retains a deferred permission %s after all content completes without keeping future refreshes open',
276+
async (pending) => {
277+
vi.stubEnv('TZ', 'Pacific/Honolulu')
278+
const now = new Date()
279+
const retryAt = new Date(now.getTime() + 30 * 60_000)
280+
const nextPeriodicRefresh = new Date(now.getTime() + 12 * 60 * 60_000)
281+
const update = (
282+
userId: string,
283+
kind: GoogleCompanyWorkUpdate['kind'],
284+
changes: Partial<GoogleCompanyWorkUpdate> = {}
285+
) =>
286+
commitGoogleCompanyWork(
287+
db,
288+
owner.connectorId,
289+
generationId,
290+
{
291+
update: {
292+
userId,
293+
kind,
294+
cursor: null,
295+
completed: true,
296+
retryAt: nextPeriodicRefresh,
297+
attempts: 0,
298+
failure: null,
299+
permissionStartedAt: null,
300+
...changes,
301+
},
302+
},
303+
generationStartedAt
304+
)
305+
for (const userId of ['first', 'second']) {
306+
await update(userId, 'permissions')
307+
await update(userId, 'content', { retryAt: new Date(now.getTime() - 60_000) })
308+
}
309+
await update('first', 'permissions', {
310+
cursor: pending === 'continuation' ? 'permission-page-9' : null,
311+
completed: false,
312+
retryAt,
313+
attempts: pending === 'failure' ? 1 : 0,
314+
failure:
315+
pending === 'failure'
316+
? {
317+
scope: first.user.email,
318+
operation: 'drive.files.list',
319+
status: 403,
320+
reasons: [],
321+
}
322+
: null,
323+
})
324+
const automatic = googleCompanyWorkStore(owner.connectorId, generationId)
325+
const manual = googleCompanyWorkStore(owner.connectorId, generationId, false)
326+
expect(await manual.remaining()).toMatchObject({ count: 1, retryAt })
327+
expect(await manual.next('content', now)).toBeNull()
328+
expect(await automatic.next('content', now)).not.toBeNull()
329+
expect(await automatic.next('permissions', now)).toBeNull()
330+
expect((await automatic.next('permissions', retryAt))?.user.id).toBe('first')
331+
for (const userId of ['first', 'second']) {
332+
await update(userId, 'content', { retryAt: new Date(now.getTime() + 2 * 60 * 60_000) })
333+
}
334+
expect(await automatic.remaining()).toMatchObject({ count: 1, retryAt })
335+
await update('first', 'permissions')
336+
expect(await automatic.remaining()).toEqual({ count: 0, retryAt: null })
337+
expect(await manual.remaining()).toEqual({ count: 0, retryAt: null })
338+
expect(await automatic.next('content', new Date(now.getTime() + 3 * 60 * 60_000))).toBeNull()
339+
}
340+
)
341+
342+
it('resets both provider continuations while preserving due permissions, and cascades on connector deletion', async () => {
343+
const permissionDueAt = new Date(Date.now() - 60_000)
344+
await db
345+
.update(knowledgeConnectorGoogleUser)
346+
.set({ permissionRetryAt: permissionDueAt })
347+
.where(eq(knowledgeConnectorGoogleUser.connectorId, owner.connectorId))
256348
await db.transaction(async (tx) => {
257349
await commitGoogleCompanyWork(
258350
tx,
@@ -285,6 +377,16 @@ describe('Google company user checkpoint storage', () => {
285377
cursor: undefined,
286378
permissionStartedAt: undefined,
287379
})
380+
const [progress] = await db
381+
.select({ permissionRetryAt: knowledgeConnectorGoogleUser.permissionRetryAt })
382+
.from(knowledgeConnectorGoogleUser)
383+
.where(
384+
and(
385+
eq(knowledgeConnectorGoogleUser.connectorId, owner.connectorId),
386+
eq(knowledgeConnectorGoogleUser.userId, 'first')
387+
)
388+
)
389+
expect(progress.permissionRetryAt).toEqual(permissionDueAt)
288390
await db.delete(knowledgeConnector).where(eq(knowledgeConnector.id, owner.connectorId))
289391
expect(
290392
await db

0 commit comments

Comments
 (0)