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
44 changes: 37 additions & 7 deletions apps/sim/app/api/knowledge/search/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,41 @@
*
* @vitest-environment node
*/
import { createEnvMock } from '@sim/testing'
import { mockNextFetchResponse } from '@sim/testing/mocks'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mockNextFetchResponse, setupGlobalFetchMock } from '@sim/testing/mocks'
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { env } from '@/lib/core/config/env'
import * as documentsUtilsModule from '@/lib/knowledge/documents/utils'

vi.mock('drizzle-orm')
vi.mock('@/lib/knowledge/documents/utils', () => ({
retryWithExponentialBackoff: (fn: any) => fn(),
}))

vi.mock('@/lib/core/config/env', () => createEnvMock())
/**
* Spy on the real documents/utils namespace instead of vi.mock: the shared
* `@/lib/knowledge/embeddings` module may be cached bound to the real module,
* so patching the namespace is the only wiring that always applies.
*/
const retrySpy = vi
.spyOn(documentsUtilsModule, 'retryWithExponentialBackoff')
.mockImplementation(((fn: () => unknown) => fn()) as never)

afterAll(() => {
retrySpy.mockRestore()
})

/**
* Under `isolate: false` the shared `@/lib/knowledge/embeddings` module may be
* cached bound to the REAL env module, so tests mutate the real `env` object
* (the tests below clear and assign it per case) instead of vi.mock'ing a
* file-local replacement that a cached consumer would never see. The snapshot
* restores whatever the worker started with after every test.
*/
const envSnapshot = { ...env }

afterEach(() => {
for (const key of Object.keys(env)) {
delete (env as Record<string, unknown>)[key]
}
Object.assign(env, envSnapshot)
})

import {
generateSearchEmbedding,
Expand All @@ -25,6 +50,11 @@ import {
describe('Knowledge Search Utils', () => {
beforeEach(() => {
vi.clearAllMocks()
// The worker-level fetch stub from vitest.setup.ts is removed after the
// first test by `unstubGlobals: true`; re-stub it per test so
// mockNextFetchResponse always operates on a mocked fetch.
setupGlobalFetchMock({ json: {} })
retrySpy.mockImplementation(((fn: () => unknown) => fn()) as never)
})

describe('handleTagOnlySearch', () => {
Expand Down
125 changes: 85 additions & 40 deletions apps/sim/app/api/knowledge/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,27 @@
* This file contains unit tests for the knowledge base utility functions,
* including access checks, document processing, and embedding generation.
*/
import { createEnvMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defaultMockEnv } from '@sim/testing'
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
import * as billingAttributionModule from '@/lib/billing/core/billing-attribution'
import { env } from '@/lib/core/config/env'
import * as documentsUtilsModule from '@/lib/knowledge/documents/utils'
import * as workspacesUtilsModule from '@/lib/workspaces/utils'

const envSnapshot = { ...env }

afterAll(() => {
for (const key of Object.keys(env)) {
delete (env as Record<string, unknown>)[key]
}
Object.assign(env, envSnapshot)
retrySpy.mockRestore()
vi.mocked(workspacesUtilsModule.getWorkspaceBilledAccountUserId).mockRestore()
vi.mocked(billingAttributionModule.assertBillingAttributionSnapshot).mockRestore()
vi.mocked(billingAttributionModule.checkAttributedUsageLimits).mockRestore()
vi.mocked(billingAttributionModule.resolveBillingAttribution).mockRestore()
vi.mocked(billingAttributionModule.toBillingContext).mockRestore()
})

vi.mock('drizzle-orm', () => ({
and: (...args: any[]) => args,
Expand All @@ -16,42 +35,54 @@ vi.mock('drizzle-orm', () => ({
sql: (strings: TemplateStringsArray, ...expr: any[]) => ({ strings, expr }),
}))

vi.mock('@/lib/core/config/env', () => createEnvMock({ OPENAI_API_KEY: 'test-key' }))

vi.mock('@/lib/knowledge/documents/utils', () => ({
retryWithExponentialBackoff: (fn: any) => fn(),
}))

vi.mock('@/lib/workspaces/utils', () => ({
getWorkspaceBilledAccountUserId: vi.fn().mockResolvedValue('user1'),
}))

vi.mock('@/lib/billing/core/billing-attribution', () => ({
assertBillingAttributionSnapshot: vi.fn((value) => value),
checkAttributedUsageLimits: vi.fn().mockResolvedValue({
/**
* Spy on the real documents/utils namespace instead of vi.mock: the shared
* `@/lib/knowledge/embeddings` module may be cached bound to the real module,
* so patching the namespace is the only wiring that always applies.
*/
const retrySpy = vi
.spyOn(documentsUtilsModule, 'retryWithExponentialBackoff')
.mockImplementation(((fn: () => unknown) => fn()) as never)

const BILLING_ATTRIBUTION_FIXTURE = {
actorUserId: 'billing-user-1',
billedAccountUserId: 'billing-user-1',
billingEntity: { type: 'user', id: 'billing-user-1' },
billingPeriod: {
start: '2026-07-01T00:00:00.000Z',
end: '2026-08-01T00:00:00.000Z',
},
organizationId: null,
payerSubscription: null,
workspaceId: 'workspace1',
} as never

function applyBillingSpies() {
vi.spyOn(workspacesUtilsModule, 'getWorkspaceBilledAccountUserId').mockResolvedValue('user1')
vi.spyOn(billingAttributionModule, 'assertBillingAttributionSnapshot').mockImplementation(
((value: unknown) => value) as never
)
vi.spyOn(billingAttributionModule, 'checkAttributedUsageLimits').mockResolvedValue({
isExceeded: false,
payerUsage: { currentUsage: 0, limit: 100 },
}),
resolveBillingAttribution: vi.fn().mockResolvedValue({
actorUserId: 'billing-user-1',
billedAccountUserId: 'billing-user-1',
billingEntity: { type: 'user', id: 'billing-user-1' },
billingPeriod: {
start: '2026-07-01T00:00:00.000Z',
end: '2026-08-01T00:00:00.000Z',
},
organizationId: null,
payerSubscription: null,
workspaceId: 'workspace1',
}),
toBillingContext: vi.fn(() => ({
} as never)
vi.spyOn(billingAttributionModule, 'resolveBillingAttribution').mockResolvedValue(
BILLING_ATTRIBUTION_FIXTURE
)
vi.spyOn(billingAttributionModule, 'toBillingContext').mockImplementation((() => ({
billingEntity: { type: 'user', id: 'billing-user-1' },
billingPeriod: {
start: new Date('2026-07-01T00:00:00.000Z'),
end: new Date('2026-08-01T00:00:00.000Z'),
},
})),
}))
})) as never)
}

/**
* Billing helpers are spied on the real namespaces (not vi.mock'd) for the
* same shared-consumer reason as the retry spy above.
*/
applyBillingSpies()

vi.mock('@/lib/knowledge/documents/document-processor', () => ({
processDocument: vi.fn().mockResolvedValue({
Expand Down Expand Up @@ -99,9 +130,8 @@ function resetDatasets() {
chunkRows = []
}

vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
function createEmbeddingFetchMock() {
return vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
data: [
Expand All @@ -111,7 +141,9 @@ vi.stubGlobal(
usage: { prompt_tokens: 2, total_tokens: 2 },
}),
})
)
}

vi.stubGlobal('fetch', createEmbeddingFetchMock())

vi.mock('@sim/db', async () => {
const { schemaMock } = (await import('@sim/testing')) as typeof import('@sim/testing')
Expand Down Expand Up @@ -261,10 +293,23 @@ describe('Knowledge Utils', () => {
dbOps.updatePayloads.length = 0
resetDatasets()
vi.clearAllMocks()
// `unstubGlobals: true` removes the module-scope fetch stub after the
// first test in the worker; re-stub it per test.
vi.stubGlobal('fetch', createEmbeddingFetchMock())
// Under `isolate: false` the shared `@/lib/knowledge/embeddings` module may
// be cached bound to the REAL env module, so reset the real `env` object
// per test instead of vi.mock'ing a file-local replacement that a cached
// consumer would never see.
for (const key of Object.keys(env)) {
delete (env as Record<string, unknown>)[key]
}
Object.assign(env, { ...defaultMockEnv, OPENAI_API_KEY: 'test-key' })
retrySpy.mockImplementation(((fn: () => unknown) => fn()) as never)
applyBillingSpies()
Comment thread
waleedlatif1 marked this conversation as resolved.
})

describe('processDocumentAsync', () => {
it.concurrent('should insert embeddings before updating document counters', async () => {
it('should insert embeddings before updating document counters', async () => {
kbRows.push({
id: 'kb1',
userId: 'user1',
Expand Down Expand Up @@ -315,7 +360,7 @@ describe('Knowledge Utils', () => {
})

describe('checkKnowledgeBaseAccess', () => {
it.concurrent('should return success for owner', async () => {
it('should return success for owner', async () => {
kbRows.push({ id: 'kb1', userId: 'user1' })
const result = await checkKnowledgeBaseAccess('kb1', 'user1')

Expand All @@ -331,7 +376,7 @@ describe('Knowledge Utils', () => {
})

describe('checkDocumentAccess', () => {
it.concurrent('should return unauthorized when user mismatch', async () => {
it('should return unauthorized when user mismatch', async () => {
kbRows.push({ id: 'kb1', userId: 'owner' })
const result = await checkDocumentAccess('kb1', 'doc1', 'intruder')

Expand All @@ -343,7 +388,7 @@ describe('Knowledge Utils', () => {
})

describe('checkChunkAccess', () => {
it.concurrent('should fail when document is not completed', async () => {
it('should fail when document is not completed', async () => {
kbRows.push({ id: 'kb1', userId: 'user1' })
docRows.push({ id: 'doc1', knowledgeBaseId: 'kb1', processingStatus: 'processing' })

Expand All @@ -370,7 +415,7 @@ describe('Knowledge Utils', () => {
})

describe('generateEmbeddings', () => {
it.concurrent('should return same length as input', async () => {
it('should return same length as input', async () => {
const result = await generateEmbeddings(['a', 'b'])

expect(result.embeddings.length).toBe(2)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'

/**
* `@/lib/auth/auth-client` builds a Better Auth client at module scope, which
* throws when NEXT_PUBLIC_APP_URL is absent from the environment (and under
* `isolate: false` an earlier file may have imported the graph in a polluted
* env). These tests only exercise pure parsing/model helpers, so stub the
* client module out entirely.
*/
vi.mock('@/lib/auth/auth-client', () => ({
useSession: vi.fn(() => ({ data: null, isPending: false })),
}))

import {
parseQuestionTagBody,
parseSpecialTags,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'

/**
* `@/lib/auth/auth-client` builds a Better Auth client at module scope, which
* throws when NEXT_PUBLIC_APP_URL is absent from the environment (and under
* `isolate: false` an earlier file may have imported the graph in a polluted
* env). These tests only exercise pure parsing/model helpers, so stub the
* client module out entirely.
*/
vi.mock('@/lib/auth/auth-client', () => ({
useSession: vi.fn(() => ({ data: null, isPending: false })),
}))

import { TOOL_CATALOG, type ToolCatalogEntry } from '@/lib/copilot/generated/tool-catalog-v1'
import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract'
import { getHiddenToolNames } from '@/lib/copilot/tools/client/hidden-tools'
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/ee/sso/components/sso-settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ function renderSso(organizationId: string) {

describe('SSO organization transitions', () => {
beforeEach(() => {
// The component reads getBaseUrl() during render; make sure the env var is
// present even when the suite runs without a local .env or after another
// test file mutated the environment (auto-restored via unstubEnvs).
vi.stubEnv('NEXT_PUBLIC_APP_URL', 'http://localhost:3000')
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
container = document.createElement('div')
document.body.appendChild(container)
Expand Down
8 changes: 4 additions & 4 deletions apps/sim/executor/handlers/agent/agent-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { setupGlobalFetchMock } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { getAllBlocks } from '@/blocks'
import { BlockType, isMcpTool } from '@/executor/constants'
Expand Down Expand Up @@ -108,13 +107,11 @@ vi.mock('@/lib/workflows/custom-tools/operations', () => ({
getCustomToolById: (...args: unknown[]) => mockGetCustomToolById(...args),
}))

setupGlobalFetchMock()

const mockGetAllBlocks = getAllBlocks as Mock
const mockExecuteTool = executeTool as Mock
const mockGetProviderFromModel = getProviderFromModel as Mock
const mockTransformBlockTool = transformBlockTool as Mock
const mockFetch = global.fetch as unknown as Mock
const mockFetch = vi.fn()
const mockExecuteProviderRequest = executeProviderRequest as Mock

describe('AgentBlockHandler', () => {
Expand All @@ -126,6 +123,9 @@ describe('AgentBlockHandler', () => {
handler = new AgentBlockHandler()
vi.clearAllMocks()

// unstubGlobals removes any module-scope fetch stub before each test, so re-stub here
vi.stubGlobal('fetch', mockFetch)

Object.defineProperty(global, 'window', {
value: {},
writable: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { getProviderFromModel } from '@/providers/utils'
import type { SerializedBlock } from '@/serializer/types'

const mockGetProviderFromModel = getProviderFromModel as Mock
const mockFetch = global.fetch as unknown as Mock
const mockFetch = vi.fn()

describe('EvaluatorBlockHandler', () => {
let handler: EvaluatorBlockHandler
Expand Down Expand Up @@ -69,6 +69,9 @@ describe('EvaluatorBlockHandler', () => {
// Reset mocks using vi
vi.clearAllMocks()

// unstubGlobals removes any module-scope fetch stub before each test, so re-stub here
vi.stubGlobal('fetch', mockFetch)

// Default mock implementations
authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValue({
accountId: 'test-vertex-credential-id',
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/executor/handlers/router/router-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
const mockGenerateRouterPrompt = generateRouterPrompt as Mock
const mockGenerateRouterV2Prompt = generateRouterV2Prompt as Mock
const mockGetProviderFromModel = getProviderFromModel as Mock
const mockFetch = global.fetch as unknown as Mock
const mockFetch = vi.fn()

describe('RouterBlockHandler', () => {
let handler: RouterBlockHandler
Expand Down Expand Up @@ -95,6 +95,9 @@ describe('RouterBlockHandler', () => {

vi.clearAllMocks()

// unstubGlobals removes any module-scope fetch stub before each test, so re-stub here
vi.stubGlobal('fetch', mockFetch)

authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValue({
accountId: 'test-vertex-credential-id',
usedCredentialTable: false,
Expand Down Expand Up @@ -394,6 +397,9 @@ describe('RouterBlockHandler V2', () => {

vi.clearAllMocks()

// unstubGlobals removes any module-scope fetch stub before each test, so re-stub here
vi.stubGlobal('fetch', mockFetch)

authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValue({
accountId: 'test-vertex-credential-id',
usedCredentialTable: false,
Expand Down
Loading
Loading