Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ jobs:
run: >-
bunx vitest run --mode integration
lib/knowledge/__integration__/search-source-progress.integration.ts
lib/knowledge/__integration__/organization-search-overview.integration.ts
lib/knowledge/__integration__/search-source-pagination.integration.ts
lib/knowledge/__integration__/search-reference-batching.integration.ts
lib/knowledge/__integration__/embedding-insert-batches.integration.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,15 @@ describe('organization source detail navigation', () => {
}
)

it('does not classify a provider message containing the permission text as its own notice', async () => {
mocks.detail.mockReturnValue({
data: { ...connector, lastSyncError: `Provider message: ${SOURCE_PERMISSION_ERROR}` },
})
await render()
expect(container.textContent).toContain('Some connection updates are incomplete')
expect(container.textContent).not.toContain('Permission verification incomplete')
})

it.each(['', '?view=settings', '?view=history'])(
'shows integration deactivation independently of source sync state at %s',
async (searchParams) => {
Expand Down
27 changes: 27 additions & 0 deletions apps/sim/lib/knowledge/__integration__/coda-live-fixture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/** @vitest-environment node */
import { describe, expect, it } from 'vitest'
import { assertCodaLiveFixture } from '@/lib/knowledge/__integration__/coda-live-fixture'

const marker = 'SimConnector-fixture'
const source = { name: `Sim Coda connector verification ${marker}`, owner: 'Owner@example.com' }

describe('Coda live fixture validation', () => {
it.each(['Owner@example.com', 'owner@example.com', ' OWNER@EXAMPLE.COM '])(
'rejects the owner as the second identity: %s',
(secondEmail) => {
expect(() => assertCodaLiveFixture(source, marker, secondEmail)).toThrow(
'Refusing to change sharing'
)
}
)

it('rejects a document outside the disposable fixture', () => {
expect(() =>
assertCodaLiveFixture({ ...source, name: 'Unrelated' }, marker, 'reader@example.com')
).toThrow('Refusing to change sharing')
})

it('accepts the disposable document with a distinct second identity', () => {
expect(() => assertCodaLiveFixture(source, marker, 'reader@example.com')).not.toThrow()
})
})
15 changes: 15 additions & 0 deletions apps/sim/lib/knowledge/__integration__/coda-live-fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { normalizeEmail } from '@sim/utils/string'

/** Checks the disposable document and distinct identities before live sharing mutations. */
export function assertCodaLiveFixture(
source: { name: string; owner: string },
marker: string,
secondEmail: string
): void {
if (
source.name !== `Sim Coda connector verification ${marker}` ||
normalizeEmail(source.owner) === normalizeEmail(secondEmail)
) {
throw new Error('Refusing to change sharing on a non-fixture document')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
} from '@/lib/billing/core/billing-attribution'
import { encryptSecret } from '@/lib/core/security/encryption'
import { createOrganizationCredential } from '@/lib/credentials/application/organization-credentials'
import { assertCodaLiveFixture } from '@/lib/knowledge/__integration__/coda-live-fixture'
import { seedKnowledgeAclFixture } from '@/lib/knowledge/__integration__/seed-source-access-fixture'
import { listKnowledgeChunks } from '@/lib/knowledge/application/chunks'
import { createKnowledgeConnector } from '@/lib/knowledge/application/connectors'
Expand Down Expand Up @@ -168,12 +169,7 @@ describe
codaDocPath(fixture.docId),
z.object({ name: z.string(), owner: z.string().email() })
)
if (
source.name !== `Sim Coda connector verification ${fixture.marker}` ||
source.owner === secondEmail
) {
throw new Error('Refusing to change sharing on a non-fixture document')
}
assertCodaLiveFixture(source, fixture.marker, secondEmail!)
fixtureValidated = true
await revokeShare()
await waitForAcl(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import {
workspace,
} from '@sim/db/schema'
import { generateId } from '@sim/utils/id'
import { eq, inArray } from 'drizzle-orm'
import { eq, inArray, sql } from 'drizzle-orm'
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import {
createKnowledgeAclFixtureIds,
seedKnowledgeAclFixture,
} from '@/lib/knowledge/__integration__/seed-source-access-fixture'
import { readOrganizationSearchOverview } from '@/lib/knowledge/application/organization-search-overview'
import { listSearchSources } from '@/lib/knowledge/application/search-sources'
import { SOURCE_PERMISSION_ERROR } from '@/lib/knowledge/connectors/sync-limits'

const ids = createKnowledgeAclFixtureIds()
const indexId = generateId()
Expand Down Expand Up @@ -177,6 +178,44 @@ async function provider(connectorType: string) {
}

describe('organization operational overview with real SQL', () => {
it.each([
SOURCE_PERMISSION_ERROR,
`Directory refresh incomplete: fixture\n${SOURCE_PERMISSION_ERROR}`,
`${SOURCE_PERMISSION_ERROR}\nSource listing failed for a fixture account`,
`Directory refresh incomplete: fixture\n${SOURCE_PERMISSION_ERROR}\nSource listing failed for a fixture account`,
])(
'recognizes a complete permission notice within composed diagnostics: %s',
async (lastSyncError) => {
await db
.update(knowledgeConnector)
.set({ lastSyncError })
.where(eq(knowledgeConnector.id, driveId))
expect(await provider('google_drive')).toMatchObject({
status: 'needs_attention',
issue: 'permission_sync_incomplete',
})
}
)

it('does not classify a provider message containing the permission text as its own notice', async () => {
await db
.update(knowledgeConnector)
.set({ lastSyncError: `Provider message: ${SOURCE_PERMISSION_ERROR}` })
.where(eq(knowledgeConnector.id, driveId))
expect(await provider('google_drive')).toMatchObject({
status: 'needs_attention',
issue: 'sync_failed',
})
})

it('ignores permission notices on paused sources', async () => {
await db
.update(knowledgeConnector)
.set({ lastSyncError: `Directory refresh incomplete: fixture\n${SOURCE_PERMISSION_ERROR}` })
.where(eq(knowledgeConnector.id, pausedDriveId))
expect(await provider('google_drive')).toMatchObject({ status: 'active', issue: null })
})

it('counts configured sources independently of viewer ACLs, and excludes workspace and untouched providers', async () => {
const result = await readOrganizationSearchOverview.execute({ principal, input })
expect(result.providers).toEqual(
Expand All @@ -188,6 +227,7 @@ describe('organization operational overview with real SQL', () => {
status: 'active',
issue: null,
isSyncing: false,
hasPendingSync: false,
},
{
connectorType: 'gmail',
Expand All @@ -196,6 +236,7 @@ describe('organization operational overview with real SQL', () => {
status: 'active',
issue: null,
isSyncing: false,
hasPendingSync: false,
},
])
)
Expand Down Expand Up @@ -237,7 +278,7 @@ describe('organization operational overview with real SQL', () => {
.where(eq(knowledgeConnector.id, gmailId))
expect(await provider('gmail')).toMatchObject({ status: 'waiting_for_connections' })
})
it('distinguishes normal member continuation from partial failure without treating idle as success', async () => {
it('distinguishes queued member continuation from active indexing and partial failure', async () => {
await db
.update(knowledgeConnectorMember)
.set({ listingCheckpoint: { cursor: 'fixture' } })
Expand All @@ -248,9 +289,15 @@ describe('organization operational overview with real SQL', () => {
connectorId: gmailId,
status: 'partial',
membersIncomplete: 1,
docsFailed: 0,
processingDispatchFailed: 0,
completedAt: new Date(),
})
expect(await provider('gmail')).toMatchObject({ status: 'indexing' })
expect(await provider('gmail')).toMatchObject({
status: 'active',
Comment thread
waleedlatif1 marked this conversation as resolved.
isSyncing: false,
hasPendingSync: true,
})
await db
.update(knowledgeConnectorMemberSyncLog)
.set({ membersFailed: 1 })
Expand All @@ -267,9 +314,13 @@ describe('organization operational overview with real SQL', () => {
expect(await provider('gmail')).toMatchObject({ status: 'needs_attention' })
await db
.update(knowledgeConnector)
.set({ nextMemberSyncAt: new Date() })
.set({ nextMemberSyncAt: sql`statement_timestamp() - interval '1 second'` })
.where(eq(knowledgeConnector.id, gmailId))
expect(await provider('gmail')).toMatchObject({ status: 'indexing' })
expect(await provider('gmail')).toMatchObject({
status: 'active',
isSyncing: false,
hasPendingSync: true,
})
await db
.update(knowledgeConnector)
.set({ nextMemberSyncAt: null })
Expand All @@ -278,6 +329,8 @@ describe('organization operational overview with real SQL', () => {
id: generateId(),
connectorId: gmailId,
status: 'completed',
docsFailed: 0,
processingDispatchFailed: 0,
startedAt: new Date(Date.now() + 1000),
completedAt: new Date(),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export const readOrganizationSearchOverview = defineAuthorizedKnowledgeUseCase({
))`,
hasAccountError: sql<boolean>`bool_or(NOT ${paused} AND ${knowledgeConnector.accessMode} = 'members' AND ${hasMemberError})`,
hasDocumentError: sql<boolean>`bool_or(NOT ${paused} AND ${hasDocumentsInState(failedDocumentCondition())})`,
hasPermissionError: sql<boolean>`bool_or(NOT ${paused} AND ${knowledgeConnector.lastSyncError} = ${SOURCE_PERMISSION_ERROR})`,
hasPermissionError: sql<boolean>`bool_or(NOT ${paused} AND ${SOURCE_PERMISSION_ERROR} = ANY(string_to_array(${knowledgeConnector.lastSyncError}, ${'\n'})))`,
hasIndexing: sql<boolean>`bool_or(NOT ${paused}
AND (${knowledgeConnector.accessMode} <> 'members' OR ${hasActiveMembers} OR ${knowledgeConnector.credentialId} IS NOT NULL)
AND (
Expand Down
Loading