Skip to content

Commit 09cab78

Browse files
committed
fix(search): correct onboarding and live citations
1 parent 90e3b91 commit 09cab78

12 files changed

Lines changed: 429 additions & 119 deletions

File tree

‎apps/sim/app/o/[organizationId]/home/components/get-started/get-started.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function StepMark({ complete }: { complete: boolean }) {
7171
* The organization home's onboarding list under the composer. Same chrome as
7272
* the workspace home's suggested actions: a hover-revealed disclosure header
7373
* over hairline-separated rows. Each step leads to the page that completes it,
74-
* and reads as done from the organization's real state: a connected account and an OAuth app authorized to use Search.
74+
* and reads as done from the organization's real state: a configured integration and an OAuth app authorized to use Search.
7575
*/
7676
export function GetStarted() {
7777
const { organization, viewer, connectedAccountsAvailable } = useOrganizationContext()
@@ -142,7 +142,15 @@ export function GetStarted() {
142142
'connect-sim-search': routes.settingsSection('search-mcp'),
143143
}
144144
const completed: Record<StepId, boolean> = {
145-
'connect-integration': Boolean(hasSearchConnection),
145+
'connect-integration': Boolean(
146+
hasSearchConnection ||
147+
integrations?.some(
148+
(integration) =>
149+
integration.approved &&
150+
integration.available !== false &&
151+
integration.configuredServiceSource
152+
)
153+
),
146154
'connect-sim-search': hasSearchAuthorization,
147155
}
148156
const steps = STEPS.filter((step) =>

‎apps/sim/lib/api/contracts/knowledge/search-integrations.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type SearchIntegrationApproval = z.output<typeof searchIntegrationApprova
1313

1414
export const searchIntegrationStatusSchema = searchIntegrationApprovalSchema.extend({
1515
available: z.boolean().optional(),
16+
configuredServiceSource: z.boolean().optional(),
1617
})
1718
export type SearchIntegrationStatus = z.output<typeof searchIntegrationStatusSchema>
1819

‎apps/sim/lib/knowledge/__integration__/search-mcp-setup.integration.ts‎

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
credential,
44
credentialGroup,
55
credentialGroupEnrollment,
6+
knowledgeBase,
7+
knowledgeConnector,
68
mcpServers,
79
member,
810
organization,
@@ -35,6 +37,10 @@ import {
3537
approveSearchIntegration,
3638
listSearchIntegrations,
3739
} from '@/lib/knowledge/application/search-integrations'
40+
import {
41+
GITHUB_INSTALLATION_PROVIDER_ID,
42+
type GitHubInstallationBinding,
43+
} from '@/lib/oauth/github-installation-types'
3844
import { defaultLiveSearchPolicy } from '@/lib/sim-search/live/policy-schema'
3945
import { SLACK_RTS_USER_SCOPES } from '@/lib/sim-search/live/scopes'
4046

@@ -247,6 +253,204 @@ describe('atomic organization live Search MCP setup', () => {
247253
}
248254
)
249255

256+
async function seedServiceSource(provider: 'google_drive' | 'github' | 'gitlab') {
257+
const knowledgeBaseId = generateId()
258+
const connectorId = generateId()
259+
const credentialId = generateId()
260+
const installation = {
261+
type: 'github_app_installation',
262+
version: 1,
263+
appId: '1',
264+
appClientId: 'fixture-github-app',
265+
installationId: '21',
266+
accountId: '11',
267+
accountType: 'Organization',
268+
accountLogin: 'fixture-owner',
269+
repositorySelection: 'selected',
270+
} satisfies GitHubInstallationBinding
271+
const encryptedInstallation =
272+
provider === 'github' ? await encryptSecret(JSON.stringify(installation)) : undefined
273+
await db.insert(knowledgeBase).values({
274+
id: knowledgeBaseId,
275+
userId: ids.owner,
276+
organizationId: ids.organization,
277+
isSearchIndex: true,
278+
name: 'Service source fixture',
279+
})
280+
await db.insert(credential).values({
281+
id: credentialId,
282+
organizationId: ids.organization,
283+
type: 'service_account',
284+
providerId: provider === 'github' ? GITHUB_INSTALLATION_PROVIDER_ID : 'google-drive',
285+
...(encryptedInstallation
286+
? {
287+
encryptedServiceAccountKey: encryptedInstallation.encrypted,
288+
providerSubjectId: installation.installationId,
289+
providerTenantId: installation.accountId,
290+
authorizationAppId: installation.appClientId,
291+
}
292+
: {}),
293+
displayName: 'Service source fixture',
294+
createdBy: ids.owner,
295+
})
296+
await db.insert(knowledgeConnector).values({
297+
id: connectorId,
298+
knowledgeBaseId,
299+
connectorType: provider,
300+
credentialId,
301+
encryptedApiKey: provider === 'gitlab' ? 'synthetic-encrypted-key' : null,
302+
sourceConfig:
303+
provider === 'github'
304+
? { repository: 'fixture-owner/repository', githubRepositoryId: '101' }
305+
: {},
306+
accessMode: provider === 'github' ? 'members' : 'admin',
307+
status: 'active',
308+
})
309+
await db.insert(organizationSearchIntegration).values({
310+
organizationId: ids.organization,
311+
connectorType: provider,
312+
approved: true,
313+
})
314+
await db
315+
.update(organization)
316+
.set({
317+
metadata: {
318+
liveSearchPolicies: {
319+
[provider]: {
320+
...defaultLiveSearchPolicy(provider),
321+
accessMode: 'service_account',
322+
...(provider === 'google_drive' ? { sourceId: connectorId } : {}),
323+
},
324+
},
325+
},
326+
})
327+
.where(eq(organization.id, ids.organization))
328+
return { knowledgeBaseId, connectorId, credentialId }
329+
}
330+
331+
async function integrationStatus(provider: string) {
332+
const data = await listSearchIntegrations.execute({
333+
principal: createSessionPrincipal({ userId: ids.member, sessionId: generateId() }),
334+
input: { organizationId: ids.organization },
335+
})
336+
return listSearchIntegrationsContract.response.schema
337+
.parse({ success: true, data })
338+
.data.find((entry) => entry.connectorType === provider)
339+
}
340+
341+
it.each(['google_drive', 'github', 'gitlab'] as const)(
342+
'reports a configured %s service source without requiring a member account',
343+
async (provider) => {
344+
const source = await seedServiceSource(provider)
345+
expect((await snapshot()).groups).toEqual([])
346+
expect(await integrationStatus(provider)).toMatchObject({ configuredServiceSource: true })
347+
await db
348+
.update(knowledgeConnector)
349+
.set({ status: 'disabled' })
350+
.where(eq(knowledgeConnector.id, source.connectorId))
351+
expect(await integrationStatus(provider)).toMatchObject({ configuredServiceSource: false })
352+
await db
353+
.update(knowledgeConnector)
354+
.set({ status: 'active', archivedAt: new Date() })
355+
.where(eq(knowledgeConnector.id, source.connectorId))
356+
expect(await integrationStatus(provider)).toMatchObject({ configuredServiceSource: false })
357+
await db
358+
.update(knowledgeConnector)
359+
.set({ archivedAt: null })
360+
.where(eq(knowledgeConnector.id, source.connectorId))
361+
await db
362+
.update(knowledgeBase)
363+
.set({ deletedAt: new Date() })
364+
.where(eq(knowledgeBase.id, source.knowledgeBaseId))
365+
expect(await integrationStatus(provider)).toMatchObject({ configuredServiceSource: false })
366+
await db
367+
.update(knowledgeBase)
368+
.set({ deletedAt: null })
369+
.where(eq(knowledgeBase.id, source.knowledgeBaseId))
370+
await db
371+
.update(organizationSearchIntegration)
372+
.set({ approved: false })
373+
.where(eq(organizationSearchIntegration.organizationId, ids.organization))
374+
expect(await integrationStatus(provider)).toMatchObject({ configuredServiceSource: false })
375+
}
376+
)
377+
378+
it('requires the selected service source to belong to this organization and provider', async () => {
379+
const source = await seedServiceSource('google_drive')
380+
const otherOrganizationId = generateId()
381+
await db.insert(organization).values({
382+
id: otherOrganizationId,
383+
name: 'Other service fixture',
384+
slug: otherOrganizationId,
385+
})
386+
try {
387+
await db
388+
.update(knowledgeBase)
389+
.set({ organizationId: otherOrganizationId })
390+
.where(eq(knowledgeBase.id, source.knowledgeBaseId))
391+
expect(await integrationStatus('google_drive')).toMatchObject({
392+
configuredServiceSource: false,
393+
})
394+
await db
395+
.update(knowledgeBase)
396+
.set({ organizationId: ids.organization })
397+
.where(eq(knowledgeBase.id, source.knowledgeBaseId))
398+
await db
399+
.update(knowledgeConnector)
400+
.set({ connectorType: 'confluence' })
401+
.where(eq(knowledgeConnector.id, source.connectorId))
402+
expect(await integrationStatus('google_drive')).toMatchObject({
403+
configuredServiceSource: false,
404+
})
405+
await db
406+
.update(knowledgeConnector)
407+
.set({ connectorType: 'google_drive' })
408+
.where(eq(knowledgeConnector.id, source.connectorId))
409+
await db
410+
.update(organization)
411+
.set({
412+
metadata: {
413+
liveSearchPolicies: {
414+
google_drive: {
415+
...defaultLiveSearchPolicy(),
416+
accessMode: 'service_account',
417+
sourceId: generateId(),
418+
},
419+
},
420+
},
421+
})
422+
.where(eq(organization.id, ids.organization))
423+
expect(await integrationStatus('google_drive')).toMatchObject({
424+
configuredServiceSource: false,
425+
})
426+
} finally {
427+
await db.delete(organization).where(eq(organization.id, otherOrganizationId))
428+
}
429+
})
430+
431+
it('does not count a GitHub member source without its active installation credential', async () => {
432+
const source = await seedServiceSource('github')
433+
await db
434+
.update(credential)
435+
.set({ revokedAt: new Date() })
436+
.where(eq(credential.id, source.credentialId))
437+
expect(await integrationStatus('github')).toMatchObject({ configuredServiceSource: false })
438+
await db
439+
.update(credential)
440+
.set({ revokedAt: null })
441+
.where(eq(credential.id, source.credentialId))
442+
await db
443+
.update(knowledgeConnector)
444+
.set({ memberSyncStatus: 'disabled' })
445+
.where(eq(knowledgeConnector.id, source.connectorId))
446+
expect(await integrationStatus('github')).toMatchObject({ configuredServiceSource: false })
447+
await db
448+
.update(knowledgeConnector)
449+
.set({ memberSyncStatus: 'idle', sourceConfig: {} })
450+
.where(eq(knowledgeConnector.id, source.connectorId))
451+
expect(await integrationStatus('github')).toMatchObject({ configuredServiceSource: false })
452+
})
453+
250454
it('keeps disabled Zoom approvals visible and removable without permitting reapproval', async () => {
251455
const connectorType = 'zoom'
252456
await db.insert(organizationSearchIntegration).values({

0 commit comments

Comments
 (0)